View的加载和更新流程

一、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,这个原理留给读者自己来研究。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值