子线程更新UI原理

作为Android开发者,曾经谨记一条铁律:不能在非UI线程中更新UI! 但是,当有一天写了下面一段代码,并成功执行后,就对曾经的铁律产生了质疑:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        new Thread(new Runnable() {
            @Override
            public void run() {
                textView.setText("hello word!");
            }
        }).start();
      
    }
}
复制代码

如果将上述代码复制到OnResume()中执行,也可以正确的执行。子线程不能更新ui好像也不是正确的结论,想要弄清楚到底怎么回事,那就需要了解Android系统中View的更新原理。

总览

Android 中 Activity 是作为应用程序的载体存在,代表着一个完整的用户界面,提供了一个窗口来绘制各种视图,想要知道View的更新原理,首先需要对Activity视图的组成有一个宏观的了解。

916005-20191123165341106-2015631979.png 上图是 View 和 Activity 之间的关系。先解释图中一些类的作用以及相关关系:

  • Activity :  对于每一个 activity 都会有拥有一个 PhoneWindow。
  • PhoneWindow :该类继承于 Window 类,是 Window 类的具体实现,即我们可以通过该类具体去绘制窗口。并且,该类内部包含了一个 DecorView 对象,该 DectorView 对象是所有应用窗口的根 View。
  • DecorView 是一个应用窗口的根容器,它本质上是一个 FrameLayout。DecorView 有唯一一个子 View,它是一个垂直 LinearLayout,包含两个子元素,一个是 TitleView( ActionBar 的容器),另一个是 ContentView(窗口内容的容器)。
  • ContentView :是一个 FrameLayout(android.R.id.content),我们平常用的 setContentView 就是设置它的子 View 。
  • WindowManager : 是一个接口,里面常用的方法有:添加View,更新View和删除View。主要是用来管理 Window 的。WindowManager 具体的实现类是WindowManagerImpl。最终,WindowManagerImpl 会将业务交给 WindowManagerGlobal 来处理。
  • ViewRootImpl: View事件处理的起点,WindowManagerGlobal将所有View的更新等事件全部交由它处理。
  • WindowManagerService (WMS) : 负责管理各 app 窗口的创建,更新,删除, 显示顺序。运行在 system_server 进程。

起因

已知View的绘制流程发起点为ViewRootImpl#RequestLayout(),而之所以子线程中不能更新UI的原因,是由于在ViewRootImpl#RequestLayout的时候进行了UI线程检查,因此就有了子线程不能更新UI的说法。

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread(); //线程检查
        mLayoutRequested = true;
        scheduleTraversals();
    }
}
复制代码

那么,接下来的问题就是排查ViewRootImpl#RequestLayout()最早被调用的时机。要想知道ViewRootImpl#RequestLayout()最早的调用时机,先要知道ViewRootImpl在什么被创建出来,深入Activity的启动流程,可以知道,View的创建肯定在Window创建之后完成,先来看看Window是什么时候创建的。

Window的创建

当一个Activity启动的时候,必须创建出一个Window来展示视图,当StartActivity的时候,会将启动activity的信息传递给AMS,而AMS会进行一系列的处理逻辑后,通过ActivityThread的performLaunchActivity()来调用Activity的attach()方法,而window就是在执行activity的attach()的时候创建。

#android.app.Activity#attach

final void attach(/**一堆参数*/) {
    //...
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    //...
   mWindow.setWindowManager(
        (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
        mToken, mComponent.flattenToString(),
        (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    //...
}
复制代码

可以看到,在Activity创建PhoneWindow的时候同时也创建了WindowManagerService并绑定到PhoneWindow。

在Activity完成了PhoneWindow的创建和绑定了WMS后,ActivityThread就调用了Activity的OnCreate()那么也就意味着其实在OnCreate()执行之前,并没有关于View的处理。

DecorView的创建

现在回头来看Activity#OnCreate(),最先执行的就是super.onCreate和setContentView,而setContentView()以后就可以处理View的逻辑了,那么DecorView的创建和绑定肯定就是在setContentView()中完成的,具体流程如下:

企业微信截图_8afd573a-2eb6-4f73-99b7-c6c096e5ad57.png 在PhoneWindow的generateDecor()中创建了DecorView并将PhoneWindow设置给了DecorView。

到目前为止,创建了PhoneWindow并绑定了WMS,并且PhoneWindow中也持有了DecorView。但是,并没有发现还是没有发现ViewRootImpl的创建,那么ViewRootImpl应该是在Activity#onResume的时候创建的,那就去看看Activity#OnResume的调用时机。

ViewRootImpl的创建

Activity的生命周期入口都需要去ActivityThread中寻找,当担OnResume()方法的调用也不例外,它是在ActivityThread#handleResumeActivity中调用的,来看看这个方法:

#android.app.ActivityThread#handleResumeActivity

 @Override
    public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
            boolean isForward, String reason) {
        //...
        if (!performResumeActivity(r, finalStateRequest, reason)) {
            return;
        }
        //...
        if (r.window == null && !a.mFinished && willBeVisible) {
           //...
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                } else {
                   //...
                    a.onWindowAttributesChanged(l);
                }
            }
        } else if (!willBeVisible) {
            if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
            r.hideForNow = true;
        }
        //...
        if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
            if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward);
            ViewRootImpl impl = r.window.getDecorView().getViewRootImpl();
            WindowManager.LayoutParams l = impl != null
                    ? impl.mWindowAttributes : r.window.getAttributes();
            if ((l.softInputMode
                    & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
                    != forwardBit) {
                l.softInputMode = (l.softInputMode
                        & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
                        | forwardBit;
                if (r.activity.mVisibleFromClient) {
                    ViewManager wm = a.getWindowManager();
                    View decor = r.window.getDecorView();
                    wm.updateViewLayout(decor, l);
                }
            }

            r.activity.mVisibleFromServer = true;
            mNumVisibleActivities++;
            if (r.activity.mVisibleFromClient) {
                r.activity.makeVisible();
            }
        }
        //...
    }

复制代码

在上述代码中,可以看到performResumeActivity()函数会被调用,我们也知道这个函数最终会调用到Activity#onResume中,当它执行完成后,下面有一段很重要的code:

if (a.mVisibleFromClient) {
    if (!a.mWindowAdded) {
        a.mWindowAdded = true;
        wm.addView(decor, l);
    } else {
        a.onWindowAttributesChanged(l);
    }
}
复制代码

这里有一个decor被添加到了wm中,wm其实就是WindowManagerImpl,而它的是WindowManagerGlobal的包装类,WindowManagerGlobal完成了WindowManagerImpl的所有的方法调用。比如addView、updateViewLayout。

#android.view.WindowManagerGlobal

public void addView(/**一堆参数*/) {
    //...
    root = new ViewRootImpl(view.getContext(), display);

    view.setLayoutParams(wparams);

    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);

    //...
}

复制代码

可以看到,ViewRootImpl被创建出来了。而且在ActivityThread#handleResumeActivity中可以看到,在wm.addView()之后,就调用了wm.updateViewLayout()。后面调用如下图:

graph TD
WindowManagerImpl.updateViewLayout --> WindowManagerGolbal.updateViewLayout --> ViewRootImpl.setLayoutParams --> ViewRootImpl.requestLayout

找到了RequestLayout也就意味着View的绘制流程就要开始了,那么也就会有线程检查的逻辑,下面来梳理一下:

  1. Activity#onCreate执行时,window已经被创建了,也成功绑定了WindowManagerService,此时还未创建DecorView
  2. Activity#setContentView执行后,decorView被创建,可以处理View的逻辑了并且没有线程检查。
  3. Activity#OnResume执行时,ViewRootImpl也没有创建,也就没有线程检查,那么在子线程中处理View的逻辑也没有问题
  4. Activity#OnResume执行后,ViewRootImpl已经被创建了,所以此时如果在子线程中更新View就会有线程检查,必然报错

现在应该已经明白了为什么子线程可以更新View,为什么不能更新View的原理了,借助这个启动优化是不是可以安排了呢?

OnResume中Measure View宽高不准确原理

在了解了子线程中能更新Ui的原理后,想着按照理解,onResume()中应该是不能获取View的宽高等数据的。那为什么下面代码是可以正确获取View的宽高呢?

@Override
protected void onResume() {
    super.onResume();
    // 不能
    int h1 = textView.getHeight();
    // 不能
    new Handler().post(new Runnable() {
        @Override
        public void run() {
            int h2 = textView.getHeight();
        }
    });
    // 能
    textView.post(new Runnable() {
        @Override
        public void run() {
            int h3 = textView.getHeight();
        }
    });
}
复制代码

有了上面子线程更行UI的理论知识后,可以很轻松的明白h1为什么不能获取到View的高度了,这里在啰嗦一点,在ViewRootImpl#RequestLayout的时候,就会很快执行View的三大方法:

企业微信截图_8c52eafb-2f1c-4296-98ce-8f264ad87e19.png

h2的获取是通过Handler的方式来获取,那么也就意味着post了一个meassage给UI线程中的Looper去处理了。在Android中,Handler消息都是以meassage为处理单元,并且Activity/Fragment等组件的生命周期也是通过meassage的方式发送给UI线程来处理的。

那么问题来了,既然OnResume是某个meassage执行中的一部分逻辑,那么在OnResume的时候post一个meassage就需要当前meassage执行完成,才能执行被post的meassage,按照子线程更新UI的原理来看的话,在OnResume所在的这个meassage中会进行View的第一次RequestLayout,那么在后面的meassage中获取View的高度应该没有问题,但是,结果确不对,难道是这两个meassage的执行顺序发生了变化?确实如此!

# android.view.ViewRootImpl

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 同步消息屏障
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

复制代码

通过上述代码,可以看到,View的真正执行时通过mChoreographer发送了一个Runable来执行的,Choreographer的原理可以看:android 图形系统之Choreographer,也就是说此meassage已经处理完成了,等到Choreographer有Sync信号过来之后再执行requestLayout的真正逻辑,那么也就意味着刚才post的那个meassage在requestLayout之前被执行了。虽然在等待Choreographer有Sync信号前发送了一个Handler的同步消息屏障的消息,但是奈何消息屏障的meassage是在post的消息之后执行。

下面看view.post为什么可以获取到正确的高度:

textView.post(new Runnable() {
    @Override
    public void run() {
        int h3 = textView.getHeight();
    }
});
复制代码

简单来说:这种方式能获取view的高度是因为:View以Handler为基础,View.post() 将传入任务的执行时机调整到View 绘制完成之后,下面来看代码:

public boolean post(Runnable action) {
    // ...
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    getRunQueue().post(action); 
    return true;
}
复制代码

通过上述代码,可以看到,View将post分为了两个支线:

  1. 当AttachInfo不为null时,直接调用其内部Handler的post;
  2. 当AttachInfo为null时,则将任务加入当前View的等待队列中。

AttachInfo不为null dispatchAttachedToWindow()调用时机是在 View 绘制流程的开始阶段,即 ViewRootImpl.performTraversals(),暂时不做讨论。 AttachInfo为null onResume中view.post主要是AttachInfo为null的情况:

 # HandlerActionQueue.post()
 
public void post(Runnable action) {
    // ...
    postDelayed(action, 0); 
}

public void postDelayed(Runnable action, long delayMillis) {
    final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

    synchronized (this) {
        if (mActions == null) {
            mActions = new HandlerAction[4];
        }

        mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
        mCount++;
    }
}

private static class HandlerAction {
    final Runnable action;
    final long delay;

    public HandlerAction(Runnable action, long delay) {
        this.action = action;
        this.delay = delay;
    }
   // ...
}
复制代码

可以看到,View.post会将Runnable封装成一个HandlerAction对象,然后创建一个默认长度为4的HandlerAction数组,用于保存post过来的任务。当View被真正attach到window上以后,就会执行post过来的Runnable,那么在onResume中view.post可以获取View的正确高度也就明白了。

上述一些查看源码后梳理,欢迎大家指正讨论!!!

猜你喜欢

转载自juejin.im/post/7107603327122309157
今日推荐