[聚合文章] 非主线程中更新UI的方式对比

消息系统 2017-12-18 102 阅读

我们知道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执行任务

注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。