我们知道Android中如果需要更新UI是需要运行在主线程中,如果我们在子线程中执行 View.invalidate
,那会报错。为什么一定要在主线程中才能更新 UI 呢?因为渲染UI的任务是在RenderThread中,如果所有线程都能与RenderThread通信,那肯定会涉及到线程同步和线程安全问题,会影响更新效率。Android中在非主线程更新UI的方式有很多种,下面一一介绍。
runOnUiThread
runOnUiThread是Activity中的一个可以在主线程中运行代码的函数,查看源码可以知道,其会先判断是否在主线程,如果是就立即执行,如果不是就会添加到主线程的消息队列中 :
final Handler mHandler = new Handler(); public final void runOnUiThread(Runnable action){ if (Thread.currentThread() != mUiThread) { mHandler.post(action); } else { action.run(); } }
Handler.post
Handler 其实是作为多线程通信的工具,所以创建Handler 的时候要注意是在什么线程中创建,如果在非主线程中创建Handler 要先初始化Looper。利用Hander更新UI其实也是把任务添加到主线程的消息队列中,因为在创建Handler的时候会获取当前线程的Looper来初始化自己:
public Handler(Callback callback,boolean async){ mLooper = Looper.myLooper(); if (mLooper == null) { throw new RuntimeException( "Can't create handler inside thread that has not called Looper.prepare()"); } mQueue = mLooper.mQueue; mCallback = callback; mAsynchronous = async; }
AsyncTask
在AsyncTask中有几个重要的函数 : onPreExecute、onPostExecute、onProgressUpdate、doInBackground。其中onPreExecute、onPostExecute、onProgressUpdate是运行在主线程的。通过查看 AsyncTask的源码可知,AsyncTask中有个InternalHandler 专门处理回调,也就是说AsyncTask是建立在 ThreadPoolExecutor 和 Handler 的基础上的,也就是运行在主线程的回调都是post任务到Hander上执行的。
private static class InternalHandlerextends Handler{ public InternalHandler(){ super(Looper.getMainLooper()); } @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) @Override public void handleMessage(Message msg){ AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj; switch (msg.what) { case MESSAGE_POST_RESULT: // There is only one result result.mTask.finish(result.mData[0]); break; case MESSAGE_POST_PROGRESS: result.mTask.onProgressUpdate(result.mData); break; } } }
View.post
View.post 在 SDK23 和 SDK24中的处理方式不一样,比如在SDK 23中的源码 :
public boolean post(Runnable action){ final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.post(action); } // Assume that post will succeed later ViewRootImpl.getRunQueue().post(action); return true; }
在Android 7.0 (SDK 24)中的源码 :
public boolean post(Runnable action){ final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.post(action); } // Postpone the runnable until we know on which thread it needs to run. // Assume that the runnable will be successfully placed after attach. getRunQueue().post(action); return true; }
相同点是在post前都会检查AttachInfo 是否为null,如果不是null就会通过attachInfo.mHandler来post任务。这个AttachInfo 是在View.dispatchAttachedToWindow中设置的 :
void dispatchAttachedToWindow(AttachInfo info,int visibility){ mAttachInfo = info; ...
View 的dispatchAttachedToWindow 是由 ViewGroup 调用 :
@Override void dispatchAttachedToWindow(AttachInfo info,int visibility){ mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW; super.dispatchAttachedToWindow(info, visibility); mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW; final int count = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < count; i++) { final View child = children[i]; child.dispatchAttachedToWindow(info, combineVisibility(visibility, child.getVisibility())); } final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size(); for (int i = 0; i < transientCount; ++i) { View view = mTransientViews.get(i); view.dispatchAttachedToWindow(info, combineVisibility(visibility, view.getVisibility())); } }
那么又是谁调用了ViewGroup.dispatchAttachedToWindow,在ViewRootImpl.performTraversals中我们可以发现 :
private void performTraversals(){ ... if (mFirst) { ... if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) { host.setLayoutDirection(mLastConfiguration.getLayoutDirection()); } host.dispatchAttachedToWindow(mAttachInfo, 0); mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true); dispatchApplyInsets(host); //Log.i(mTag, "Screen on initialized: " + attachInfo.mKeepScreenOn); } ... }
这个host 其实就是 Activity 的 DecorView,所以这里有两点结论:
- 只有在ViewRootImpl.performTraversals之后,AttachInfo 才被设置,也就是说ViewRootImpl.performTraversals之后,View.post的任务才会在添加到主线程的消息队列中
- 只有是DecorView 的子 View才会去设置这个View的AttachInfo,这一点很重要,因为我们可能会在代码中直接 new一个View,如果这个View不添加到 DecorView ,那么他的AttachInfo会一直为null。
回到刚才的问题,如果是AttachInfo 为null 的情况,在SDK23 的时候会通过 ViewRootImpl.getRunQueue().post(action);
来执行任务。ViewRootImpl.getRunQueue 源码如下 :
static HandlerActionQueue getRunQueue(){ HandlerActionQueue rq = sRunQueues.get(); if (rq != null) { return rq; } rq = new HandlerActionQueue(); sRunQueues.set(rq); return rq; }
其实就是把任务都添加到HandlerActionQueue中,等到ViewRootImpl.performTraversals的时候才执行HandlerActionQueue的任务:
private void performTraversals(){ ... // Execute enqueued actions on every traversal in case a detached view enqueued an action getRunQueue().executeActions(mAttachInfo.mHandler); ... }
在Android 7.0 (SDK 24)的时候呢,任务会被 getRunQueue().post(action);
来执行 ,View.getRunQueue()源码如下:
private HandlerActionQueue getRunQueue(){ if (mRunQueue == null) { mRunQueue = new HandlerActionQueue(); } return mRunQueue; } ``` 可见View中的HandlerActionQueue 与ViewRootImpl是同一个数据结构,那么View中的在什么时候执行呢,前面说了,是在dispatchAttachedToWindow 的时候 : ```java void dispatchAttachedToWindow(AttachInfo info,int visibility) { ... // Transfer all pending runnables. if (mRunQueue != null) { mRunQueue.executeActions(info.mHandler); mRunQueue = null; } ... }
所以在Android 7.0 (SDK 24)的时候,在AttachInfo为null的时候,任务会被添加到 View自己的RunQueue中,等到View.dispatchAttachedToWindow的时候才执行。而且View.RunQueue 比ViewRootImpl.RunQueue 先执行。这里要注意一点,只有DecorView 的子View才会被执行dispatchAttachedToWindow ,也就是说只有添加到DecorView的View才会执行这个View的RunQueue。
View.postInvalidate
View.postInvalidate 也可以在子线程中去更新 UI ,其源码如下 :
public void postInvalidate(int left, int top, int right, int bottom){ postInvalidateDelayed(0, left, top, right, bottom); } public void postInvalidateDelayed(long delayMilliseconds){ // We try only with the AttachInfo because there's no point in invalidating // if we are not attached to our window final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds); } }
也就是说在attachInfo不为 null的时候才会把任务添加到主线程的消息队列中。
思考问题
在下面这段代码中,你知道打印的顺序是什么吗?
@Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); View v = new View(this); v.post(new Runnable() { @Override public void run(){ Log.d("Test", "A custom view.post"); } }); findViewById(R.id.activity_main).post(new Runnable() { @Override public void run(){ Log.d("Test", "B findViewById view.post"); } }); new Handler().post(new Runnable() { @Override public void run(){ Log.d("Test", "C Handler.post"); } }); runOnUiThread(new Runnable() { @Override public void run(){ Log.d("Test", "D main Thread runOnUiThread"); } }); new Thread(new Runnable() { @Override public void run(){ runOnUiThread(new Runnable() { @Override public void run(){ Log.d("Test", "E sub Thread runOnUiThread"); } }); } }).start(); }
我们知道 在这些方式中,runOnUiThread会检查是否在主线程,所以D 会首先执行,如果runOnUiThread不在主线程执行,会把任务添加到 Handler的消息队列(主线程消息队列)中,在前面的分析中可以知道,view.post的任务会在ViewRootImpl.performTraversals时才执行,所以执行顺序是DCE。在onCreate中,此时View的AttachInfo肯定为null,所以第一个view.post在SDK 23时会添加到 ViewRootImpl的RunQueue中,在SDK 24时会添加到View自己的RunQueue中,由于没有添加到DecorView所以不会执行,所以在执行顺序是 DCEBA(SDK 23) 或 DCEB(SDK 24)。
在SDK 23的系统中运行结果是 :
Test : D main Thread runOnUiThread Test : C Handler.post Test : E sub Thread runOnUiThread Test : B findViewById view.post Test : A custom view.post
在SDK 24的系统中运行结果是 :
Test : D main Thread runOnUiThread Test : C Handler.post Test : E sub Thread runOnUiThread Test : B findViewById view.post
总结
更新 UI 的各种方式有以下几种,从中可以发现,其实所有方式都是基于Handler来实现的:
-
runOnUiThread
- 直接执行任务或通过Handler执行
-
Handler.post
- 添加到Handler消息队列
-
AsyncTask
- 通过Handler执行
-
View.post
- AttachInfo不为null时,直接用attachInfo.mHandler执行任务
-
AttachInfo为null时 :
- 在SDK23 的时候,任务会被添加到 ViewRootImpl的RunQueue中,等到ViewRootImpl.performTraversals的时候才执行
- 在Android 7.0 (SDK 24)的时候,任务会被添加到 View自己的RunQueue中,等到View.dispatchAttachedToWindow的时候才执行,View.RunQueue 比ViewRootImpl.RunQueue 先执行,只有添加到DecorView 的View 才会执行
-
View.postInvalidate
- 只有AttachInfo不为null的时候才会利用attachInfo.mHandler执行任务
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。