View加载绘制流程
- 子线程更新UI异常问题
我们都知道,在子线程进行UI操作是不被允许的,会抛出异常。上周短信的回溯中也是出现了子线程更新UI导致的ANR/闪退问题。
报错示意图如下:

可以发现其实检查处位于ViewRootImpl里面,具体是checkThread()方法

接下来从这里探讨下View的加载过程。
- View 加载过程
从上面的异常栈能够看到,它是一层一层的向上调用requestLayout方法,最终调用到ViewRootImpl里面的方法。这种层级关系就是我们说的view树结构。
以下,我们以activity界面加载为例,探究一下view加载的过程。
我们可以调用setContentView方法来加载自己的布局。同时,安卓的界面是通过window来承载的,所以其实它也被加载到了这个window上,这个过程是怎样的呢?

其实调用这个方法之前,系统已经创建好了应用的窗口和布局,这里只是将我们自己的view添加进去,让其和activity关联起来。
- performLaunchActivity 方法
activity的onCreate() 、onResume() … 这些生命周期回调,实际上是在ActivityThread中调用的方法。如onCreate就是在performLaunchAcitity中回调。这个方法主要是创建Activity实例、创建窗口,以及建立Actvity和window的关系。
另外在performLaunchAcitity之前还有一大堆的过程,大概就是系统会为我们创建或分配应用进程,并和AMS建立联系,然后再回调到这里。
- activity 的创建
在performLaunchActivity中创建activity的方法如下:

再进入到红框的代码

可以看到由反射创建activity实例,且用了前面指定的classLoader。
- Attach 的过程
完成activity的创建后,回到performLaunchActivity方法中。接下来会调用attach方法,确定这个activity和window的关系。

里面主要创建phoneWindow和获取windowManager:

windowManager通过getSystemService直接获取。

到这里创建好了mWindow和mWindowManager。
- onCreate() 回调
再次回到performLaunchActivity(), 从attach方法继续向下,来到callActivityOnCreate()。

这里面会回调onCreate()
- setContentView() 方法
setContentView会调用到mWindow的setContentView方法。也就是2.1.2中创建的PhoneWindow。


PhoneWindow的setContentView主要做两件事情:
初始化DecorView,
加载通过参数layoutResID传入的布局文件资源。


- installDecor()
进入installDecor这个方法主要做两件事:创建Decor和创建ContentParent。

a. generateDecor()
直接new 一个DecorView,

DecorView是一个继承自FrameLayout的View。
于是可以得到下图:

b. generateLayout()
之后decorView被当做参数传到generateLayout里面,用于初始化contentParent。
ContentParent 是一个ViewGroup,通过注释我们看到,它会被添加到
DecorView 上。

而在这个generateLayout中,主要是通过主题做各种对window的配置,从而得到不同的布局。
另外:由于此时处于setContentView中就要用到各种属性,因此设置属性要在setContentView之前才能生效,如下:

之后会根据属性选择一个布局,

可以看到其中FrameLayout的id为”@android:id/content”

接下来加载布局到decorView上。

可以看到这里的contentParent就是前面的frameLayout。
之后contentParent创建完成
如下:

- inflate布局文件
完成了mDecor 和mContentParent 的初始化后,系统创建
的View 树就完成了。之后需要将自身的布局文件,放到这个布局树上。就完成了
布局加载的工作。
直接调用“mLayoutInflater.inflate(layoutResID, mContentParent);”将用户的布
局添加到mContentParent 上。

注意,inflate是通过反射解析加载view,因此要尽量减少层级嵌套,加快速度。
到此,完成整个布局加载的过程。
- ViewRootImpl 创建过程
到整个View 加载完成,ViewRootImpl 还没有出现。那么接下来看一下ViewRootImpl的创建过程。其实这部分在handleResumeActivity中。ActivityThread 中的handleResumeActivity 主要做两件事,回调onResume方法和更新View。
- performResumeActivity()
首先会执行

在里面会调用到activity的preformResume方法,并在以下回调onResume。

- addView()
回到handleResumeActivity()方法的下面部分。这里会调用wm.addView方法,实际上是在这个方法里创建了ViewRootImpl和完成对View的绘制。
这里会调用addView,实际完成了ViewRootImpl的创建以及View的绘制。

跟进addView方法,它又是通过调用mGlobal的方法实现的。

继续进入,可以看到ViewRootImpl被创建。

在ViewRootImpl的构造函数中,会记录当前线程到mThread变量。子线程更新UI报错,检查线程就是检查的这个。

WindowManagerGlobal会将view(DecorView)和root(ViewRootImpl)进行保存。然后调用ViewRootImpl的setView方法。
a. requestLayout()

这个过程完成view的测量、布局和绘制。这里会触发view的onLayout()、onMeasure()、onDraw()完成view的绘制即可。详细内容,在下一章节“View更新过程”详细讨论。
b. view.assignParent(this);

会把ViewRootImpl设置为DecorView的parent。
源码阅读到这里,我们增加了WindowManagerImpl、
WindowManagerGlobal 和 ViewRootImpl 三个内容。将这些元素加入到View
结构树中,得到一个比较完整的Activity、Window 和View 之间的关系图。
每个activity 都会有一个phoneWindow,也有
唯一的windowManager 对象。WindowManager 的操作是通过
WindowManagerGlobal 来实现,这是一个单例对象,记录了所有activity 的
decorView、viewRootImpl 信息,通过WindowManagerGlobal 方便对应用窗口
试图进行管理,理论上可以通过它拿到应用所有view 的信息。

- View 更新过程
WindowManagerImpl.addView() ->
ViewRootImpl.setView()过程中,会调用一次requestLayout()这是第一次对View
树进行自上而下的测量、布局和绘制。注意此时DecorView 还没有将
ViewRootImpl 设为Parent。是由ViewRootImpl 主动发起的刷新。后面的刷新
基本都是由子布局向上请求,再进行自上而下进行刷新。
这部分外面文档讲的很多了,也就是调用顺序scheduleTracersals() -> mTraversalRunnable -> doTraversal() -> performTraversals() -> performMeasure() > onMeasure()、performLayout() > onLayout()、performDraw() > onDraw()。
分为测量, 布局和绘制三部分。
- 子线程更新UI
- 避免requestLayout的向上调用
通过异常调用栈,我们看到在子view中不断向上调用requestLayout,最终调用到viewRootImpl的requestLayout,在其中的checkThread方法中抛出异常。因此只要view更新没有向上调用requestLayout就可以避免这个异常。

调用View的requestLayout方法,会先设置PFLAG_FORCE_LAYOUT标志,这个标志会在layout的时候进行清除。当这个标记存在时,在下面调用mParent.isLayoutRequested()是会返回true,这样就不会再向上调用requestLayout。Android这样设计的目的是避免多次触发重绘。
因此现在主线程先执行requestLayout,然后再在子线程更新UI可以避免抛出异常。

- 在子线程创建ViewRootImpl

这个mThread是在viewRootImpl创建的时候赋值的,而viewRootImpl是在windowManager.addView里面被创建的。所以如果直接在子线程addView,那之后在这个线程更新view是没问题的。

- 在onResume之前更新
因为viewRootImpl的创建是在performResumeActivity里面,也就是onResume之后。因此在onResume之前的生命周期里面更新UI,就不会到达ViewRootImpl里面。
但该情况在子线程应避免做耗时操作,否则可能等到viewRootImpl已经被创建并绑定给decorView。