一、View加载过程
我们知道,我们是在onCreate方法中调用setContentView方法来加载我们自己的布局。通过这个方法,我们可以将我们的布局添加到当前的窗口。那么这个过程是怎样的呢?
实时上,通过调用setContentView方法,既是将activity和用户写的view关联起来。早在这个方法调用之前,系统已经为我们创建了应用的窗口和布局,这个方法,只是将用户的view添加进来。
1.1 performLaunchAcitity()方法
我们看到的onCreate() 、onResume() … 这些生命周期回调,实际上是在ActivityThread中调用的方法。如onCreate就是在performLaunchAcitity中回调。这个方法主要是创建Activity实例、创建窗口,以及建立Actvity和window的关系。
在performLaunchAcitity之前还有一大堆的过程,大概就是系统会为我们创建或分配应用进程,并和AMS建立联系,然后再回调到这里。这个过程和本文主题关系不大,读者可以自行了解下activity的启动过程。
1.1.1 Activity的创建
先来看一下Activity是如何创建的。在performLaunchAcitity方法中。
可以看到Activity的创建,就是通过反射创建他的实例对象。
1.1.2 Attach方法
在activity被创之后,依然在performLaunchAcitity()方法中,我们继续向下看。调用attach方法,建立和window之间关系。
这里主要new phoneWindow和创建windowManager:
windowManager是通过WMS进行获取。
到这里我们知道mWindow和mWindowManager是在这里创建出来,他们分别就是PhoneWindow和WindowManager。先记录一下,后面会用到。
1.1.3 onCreate()方法回调
依然回到performLaunchAcitity(),从attach方法再向下,可以看到调用callActivityOnCreate(),这里会回调我们的onCreate()生命周期方法。
我们知道在onCreate()方法中会调用setContentView方法。
1.2 setContentView()方法
setContentView会调用到mWindow的setContentView方法。通过前面的介绍,我们知道mWindow既是PhoneWindow。
PhoneWindow的setContentView主要做两件事情:初始化DecorView和加载我们的布局。他的参数layoutResID就是我们布局文件的资源ID。
1.2.1 installDecor()
进入instarllDecor这个方法主要做两件事:创建Décor和创建ContentParent。我们先跟进去看下这两个过程,弄清楚他们分别是什么。
创建DecorView
这里实际上就是直接new DecorView出来。
我们看到DecorView继承自FrameLayout,因此实际它是一个View。
“public class DecorView extends FrameLayout...”
到此我们可以得到Activity、Window、View他们之间的关系:
创建ContentParent
DecorView创建之后,继续向下看,它被当作参数传给generateLayout()方法,用于初始化mContentParent。
ContentParent是一个ViewGroup,通过注释我们看到,它会被添加到DecorView上。接下来跟一下generateLayout的方法。
进入这个方法我们首先看到大量的requestFeature()方法,这些feature有我们经常会用到的如“FEATURE_NO_TITLE” 、“FLAG_FULLSCREEN”、“FEATURE_ACTION_BAR_OVERLAY”等等。它实际上是根据主题做一些window的配置。
这里插一个题外话:我们注意到现在是进行setContentView的过程。这也就是为什么我们在设置窗口属性时,要在setContentView方法执行之前。因为,这些窗口属性是在setContentView的过程中被用到。在setContentView方法之后设置是不生效的。
回到创建mContentView的过程。配置窗口属性之后,根据属性,选择一个系统的布局文件。
我们已最基本的布局为例看这个过程。此时选中的时screen_simple这个布局文件。它在sdk目录下,是系统为我们提供的布局文件。
一个很简单的线性布局文件,包含ViewStub和FrameLayout两个部分。注意FrameLayout的id是 “@android:id/content”,这个很重要,后面会用到。
layoutResource确定之后,调用mDecor.onResourcesLoaded()将布局加载到DecorView上。
通过findeViewById获取contentParent。我们看到这个布局id就是“R.id.content”。既是上面screen_simple.xml中的FrameLayout。
最后直接返回contentParent,完成mContentParent的创建过程。由此,我们知道mContentParent就是系统布局文件里的FrameLayout。
到这里我们进一步完善DecorView的内容。可以看到这里是系统为我们建立的View树的结构。
1.2.2 inflate用户布局文件
完成了mDecor和mContentParent的初始化。我们得到了系统为我们创建的View树。接下来,只需要将我们的布局文件,放到这个布局树上。就完成了布局加载的工作。这就是,前面说的setContentView的第二个阶段做的工作。直接调用“mLayoutInflater.inflate(layoutResID, mContentParent);”将用户的布局添加到mContentParent上。
到此,完成整个布局加载的过程。
2、ViewRootImpl创建过程
接下来看一下ViewRootImpl的创建过程。我们知道在onCreate生命周期回调之后,会调用onStart和onResume方法。我们说的ViewRootImpl的创建跟onResume方法在一个生命周期。
ActivityThread中的handleResumeActivity主要做两件事,回调onResume方法和更新View。
2.1 performResumeActivity
handleResumeActivity()方法首先会调用performResumeActivity方法。这里会回调onResume方法。
我们看到在onResume方法被回调的时候,view还是没有被显示出来。所以,网上有一种说法,onResume时候,view变成可见状态是不准确的。
2.2 addView
performResumeActivity执行之后,继续看handleResumeActivity()方法的下面部分。
这里会调用wm.addView方法,实际上是在这个方法里创建了ViewRootImpl和完成对View的绘制。
ViewManager是一个接口,WindowManager也是一个接口,它实现了ViewManager接口。
前面我们在说到创建PhoneWindow时候,同时通过WMS也创建了WindowManager。在这里它的实现类是WindowManagerImpl。
我们看一下这个的addView方法,它又是通过调用mGlobal的方法实现的。
mGlobal是WindowManagerGlobal,它是一个单例类。
继续看WindowManagerGlobal的addView方法。在这个方法中,我们看到”new ViewRootImpl” 这是ViewRootImpl被创建的位置,也是唯一被创建的位置。
在ViewRootImpl的构造函数中,会记录当前线程到mThread变量。前面我们说子线程更新UI报错,检查线程就是检查的这个。
WindowManagerGlobal会将view(DecorView)和root(ViewRootImpl)进行保存。然后调用ViewRootImpl的setView方法。
这个方法很长,我们看几个主要的点:
1、requestLayout()
这个过程完成view的测量、布局和绘制。这个过程比较的长而且也很重要。现在先放一下。我们只要知道这里会触发view的onLayout()、onMeasure()、onDraw()完成view的绘制即可。详细内容,我们在下一章节“View更新过程”详细讨论。
2、view.assignParent(this);
这个方法就是设置ViewRootImpl为DecorView的parent。这个方法最终调用到父类View中的方法:
源码阅读到这里,我们增加了WindowManagerImpl、WindowManagerGlobal 和 ViewRootImpl三个内容。将这些元素加入到View结构树中,得到一个比较完整的Activity、Window和View之间的关系图。
通过前面的分析,我们知道每个activity都会有一个phoneWindow,也有唯一的windowManager对象。WindowManager的操作是通过WindowManagerGlobal来实现,这是一个单例对象,记录了所有activity的decorView、viewRootImpl信息,通过WindowManagerGlobal方便对应用窗口试图进行管理,理论上可以通过它拿到应用所有view的信息。
3、View更新过程
上一节说到在ViewRootImpl进行WindowManagerImpl.addView() -> ViewRootImpl.setView()过程中,会调用一次requestLayout()这是第一次对View树进行自上而下的测量、布局和绘制。注意此时DecorView还没有将ViewRootImpl设为Parent。是由ViewRootImpl主动发起的刷新。后面的刷新基本都是由子布局向上请求,再进行自上而下进行刷新。
3.1 requestLayout()
这里checkThread()就是我们刚开始说,子线程更新UI抛出异常的地方。它就是在ViewRootImpl的requestLayout()被调用,这也是唯一被调用的地方。
接下来调用顺序scheduleTracersals() -> mTraversalRunnable -> doTraversal() -> performTraversals() -> performMeasure() > onMeasure()、performLayout() > onLayout()、performDraw() > onDraw()。
3.2 performTraversals()
这个方法非常的长,有大几百行代码。我们看主要的几个点。
3.2.1 performTraversals主要流程
从performTraversals()方法开始看,此时mFirst == true,会进到如下代码块。第一次目标宽高即是取的window的宽高。
接下来调用到DecorView的dispatchAttachedToWindow(mAttachInfo, 0)方法,这个方法就是将窗口的属性设置给view。同一个Window中的一组view,它们的mAttachInfo都是同一个成员变量。可以通过mAttachInfo是否有值判断一个view是否依附于一个window。
View类的isTttachedToWindow方法是判断mAttachInfo是否为空:
接着调用“dispatchApplyInsets(host)”。这个方法就是将状态栏和导航栏加入到DecorView中来。
再向下看,mApplyInsetsdRequested == true,会进到下面的代码块,调用measureHierarchy()方法。这里实际上是将window的宽高给到DecorView。
向下看代码,进入到如下代码块,调用一次relayoutWindow方法,将初始测量的值以及参数发给WMS,确定窗口的属性值。
继续向下,这里是进行第二次的测量。第一次是确定了window的大小。这一次才是真正测量view的尺寸。
第二次测量完成后,请求布局。至这里,view的尺寸和位置被准确的确定下来。
看下面的代码,我们注意到布局完成会触发一次dispatchOnGlobalLayout()的回调。从这里源码中我们知道,在用户断注册该回调,在回调中获取view的宽高属性是最及时的。
接着进入View的绘制流程,完成view的绘制我们就能看到应用的界面。
到此,View的更新显示完成。
3.2.2 performMeasure
单独看一下performMeasure方法。这个方法会调用到View的measure()方法。然后按照view的层级,进行自上而下的测量。我们自定义view时所实现的onMeasure方法,就是做这个事情。测量将view的宽高属性记录在measureSpec中,完成测量。
3.2.3 performLayout
这个方法是在确定view尺寸的基础上,对view进行布局,就是确定view的摆放的位置。他也是自定向下逐个view进行布局。该方法会回调到View的onLayout()方法。
3.2.4 performDraw
这个方法将view绘制到界面上,它会根据配置,选择硬件绘制会软件绘制。他会回调view的onDraw()方法。完成view的绘制工作。
4、在子线程中更新UI
通过上面对view的加载和布局过程的详细跟踪分析。再来看一下我们经常遇到的“在子线程更新UI异常”的问题。分析一下能够在子线程更新UI的思路。
4.1、避免requestLayout调用
通过异常调用栈,我们看到在子view中不断向上调用requestLayout,最终调用到viewRootImpl的requestLayout,在其中的checkThread方法中抛出异常。因此只要view更新没有向上调用requestLayout就可以避免这个异常。
在子线程更新UI之前,在主线程调用一次requestLayout()方法,可以避免在子线程更新UI抛出异常。
调用View的requestLayout方法,会先设置PFLAG_FORCE_LAYOUT标志,这个标志会在layout的时候进行清楚。当这个标记存在时,在下面调用mParent.isLayoutRequested()是会返回true,这样就不会再向上调用requestLayout。Android这样设计的目的是避免多次触发重绘。
因此现在主线程先执行requestLayout,然后再在子线程更新UI可以避免抛出异常。
在layout方法中,清除PFLAG_FORCE_LAYOUT这个标志:
4.2、在子线程创建ViewRootImpl
再看一下ViewRootImpl检查线程的方法checkThread。这个方法的意思是,必须要在创建ViewRootImpl的线程中更新UI。正常ViewRootImpl都是在主线程创建,因此,我们常说成“在主线程更新UI”。其实是一个不太准确的说法。
通过之前的分析,我们知道这个mThread是在创建ViewRootImpl时,记录的创建ViewRootImpl对象的线程。而ViewRootImpl是在WindowManagerImpl执行addView方法时创建的。
因此,我们可以在子线程中创建ViewRootImpl,然后在该线程中进行UI更新。
这里有个知识点,通过在子线程中创建viewRootImpl更新UI,需要创建looper。因为view中事件也是通过handler来处理的。子线程默认是没有创建looper的,直接使用会报错。关于looper、handler有不太了解的。读者可以自行研究下handler的机制。
子线程更新UI还有其他方法,如TextView在某些场景的优化,为避免重复绘制,更新文字有时不会触发requestLayout,则可以在子线程进行;使用SurfacaView可以子线程更新UI,这个原理留给读者自己来研究。