View加载绘制流程

View加载绘制流程

  1. 子线程更新UI异常问题

我们都知道,在子线程进行UI操作是不被允许的,会抛出异常。上周短信的回溯中也是出现了子线程更新UI导致的ANR/闪退问题。

报错示意图如下:

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

接下来从这里探讨下View的加载过程。

  1. View 加载过程

从上面的异常栈能够看到,它是一层一层的向上调用requestLayout方法,最终调用到ViewRootImpl里面的方法。这种层级关系就是我们说的view树结构。

以下,我们以activity界面加载为例,探究一下view加载的过程。

我们可以调用setContentView方法来加载自己的布局。同时,安卓的界面是通过window来承载的,所以其实它也被加载到了这个window上,这个过程是怎样的呢?

其实调用这个方法之前,系统已经创建好了应用的窗口和布局,这里只是将我们自己的view添加进去,让其和activity关联起来。

  1. performLaunchActivity 方法

activity的onCreate() 、onResume() … 这些生命周期回调,实际上是在ActivityThread中调用的方法。如onCreate就是在performLaunchAcitity中回调。这个方法主要是创建Activity实例、创建窗口,以及建立Actvity和window的关系。

另外在performLaunchAcitity之前还有一大堆的过程,大概就是系统会为我们创建或分配应用进程,并和AMS建立联系,然后再回调到这里。

  1. activity 的创建

在performLaunchActivity中创建activity的方法如下:

再进入到红框的代码

可以看到由反射创建activity实例,且用了前面指定的classLoader。

  1. Attach 的过程

完成activity的创建后,回到performLaunchActivity方法中。接下来会调用attach方法,确定这个activity和window的关系。

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

windowManager通过getSystemService直接获取。

到这里创建好了mWindow和mWindowManager。

  1. onCreate() 回调

再次回到performLaunchActivity(), 从attach方法继续向下,来到callActivityOnCreate()。

这里面会回调onCreate()

  1. setContentView() 方法

setContentView会调用到mWindow的setContentView方法。也就是2.1.2中创建的PhoneWindow。

PhoneWindow的setContentView主要做两件事情:

初始化DecorView,

加载通过参数layoutResID传入的布局文件资源。

  1. 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创建完成

如下:

  1. inflate布局文件

完成了mDecor 和mContentParent 的初始化后,系统创建

的View 树就完成了。之后需要将自身的布局文件,放到这个布局树上。就完成了

布局加载的工作。

直接调用“mLayoutInflater.inflate(layoutResID, mContentParent);”将用户的布

局添加到mContentParent 上。

注意,inflate是通过反射解析加载view,因此要尽量减少层级嵌套,加快速度。

到此,完成整个布局加载的过程。

  1. ViewRootImpl 创建过程

到整个View 加载完成,ViewRootImpl 还没有出现。那么接下来看一下ViewRootImpl的创建过程。其实这部分在handleResumeActivity中。ActivityThread 中的handleResumeActivity 主要做两件事,回调onResume方法和更新View。

  1. performResumeActivity()

首先会执行

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

  1. 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 的信息。

  1. View 更新过程

WindowManagerImpl.addView() ->

ViewRootImpl.setView()过程中,会调用一次requestLayout()这是第一次对View

树进行自上而下的测量、布局和绘制。注意此时DecorView 还没有将

ViewRootImpl 设为Parent。是由ViewRootImpl 主动发起的刷新。后面的刷新

基本都是由子布局向上请求,再进行自上而下进行刷新。

这部分外面文档讲的很多了,也就是调用顺序scheduleTracersals() -> mTraversalRunnable -> doTraversal() -> performTraversals() -> performMeasure() > onMeasure()、performLayout() > onLayout()、performDraw() > onDraw()。

分为测量, 布局和绘制三部分。

  1. 子线程更新UI
  1. 避免requestLayout的向上调用

通过异常调用栈,我们看到在子view中不断向上调用requestLayout,最终调用到viewRootImpl的requestLayout,在其中的checkThread方法中抛出异常。因此只要view更新没有向上调用requestLayout就可以避免这个异常。

调用View的requestLayout方法,会先设置PFLAG_FORCE_LAYOUT标志,这个标志会在layout的时候进行清除。当这个标记存在时,在下面调用mParent.isLayoutRequested()是会返回true,这样就不会再向上调用requestLayout。Android这样设计的目的是避免多次触发重绘。

因此现在主线程先执行requestLayout,然后再在子线程更新UI可以避免抛出异常。

  1. 在子线程创建ViewRootImpl

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

  1. 在onResume之前更新

因为viewRootImpl的创建是在performResumeActivity里面,也就是onResume之后。因此在onResume之前的生命周期里面更新UI,就不会到达ViewRootImpl里面。

但该情况在子线程应避免做耗时操作,否则可能等到viewRootImpl已经被创建并绑定给decorView。