View原理细节(二)-显示流程之测量和布局

187 阅读17分钟

       本节我们来分析一下View的显示流程,根据上一节的分析,我们已经知道,当Activity的onResume函数执行完毕了之后,这个Activity对应的View Hierarchy已经构建完成,并且已经添加到显示系统中,可以被用户看到。那么接下来被显示的每个View对象就需要确定各自的大小和位置信息,并将自己需要显示的东西绘制出来。所以,针对View的显示过程包括测量,布局和绘制三个阶段。

       这三个阶段按顺序执行,环环相扣。测量阶段会确定View的大小,布局阶段就会根据测量阶段得到的大小,并考虑内外边距的情况下来确定View的位置。至此,View对象的大小和位置都确定好了,相关的属性也完成了赋值,但这依然是代码层面的,最终大小和位置这些信息被用户感知到还是需要画出来。可以这么理解,测量和布局两个阶段是为了给绘制阶段提供数据,而绘制阶段根据这些数据的限制,对相应的Canvas进行移动或者范围裁剪,最终和该View的大小和位置信息相匹配,然后View在根据需要调用Canvas的Api进行绘制,从而让View呈现出来的效果和它的大小位置相匹配。

       根据上一节的分析,ViewRootImpl的requestLayout()函数开启了对应的View Hierarchy的显示流程,而View内部也有两个类似的函数:requestLayout()和invalidate(),我们可以从这两个函数的角度来分析一下View的显示流程。需要说明的是,这里对View显示流程的分析,排除了硬件加速和setLayerType的情况,所以这里的分析,都是在非硬件加速和LAYER_TYPE_NONE的前提下分析的。对硬件加速感兴趣的朋友可以看官网或者扔物线大神的文章

一:RequestLayout

首先来看一下RequestLayout:

                                                       requestLayout()

       根据源码的解析,当某个View的layout invalidated的时候可以调用这个函数。也就是说,当某个View的大小或者位置发生了改变,可以调用这个函数。调用完了之后,就会在View所在的ViewTree里通过mParent来进行layout pass,而ViewGroup内部并没有对requestLayout()函数进行重写,所以会逐步向上传递,最终调用到ViewRootImpl的requestLayout函数。

       这里需要注意的是,函数给当前View设置了两个flag:PFLAG_FORCE_LAYOUT和PFLAG_INVALIDATED。首先,View内部有个int类型的mPrivateFlags字段,这个字段会View的整个显示流程进行控制,就相当于我们平时开发时,自定义的一系列的boolean值。它的工作原理是位运算,这里拿PFLAG_FORCE_LAYOUT举例,通过调用mPrivateFlags |= PFLAG_FORCE_LAYOUT将mPrivateFlags的PFLAG_FORCE_LAYOUT位设置为1,不设置的情况下是0。当设置为1的时候,意味着当前View有个操作需要执行或者某项操作已经执行完了,然后设置成1标记状态。除了PFLAG_FORCE_LAYOUT,View内部还有很多的Flag位,后面我们会一一看到。那这里将PFLAG_FORCE_LAYOUT位设置位1,就意味着当前的View需要重新布局,重新显示,后面也会有对这个标志位的判断。

       接下来就会进入到ViewRootImpl的requestLayout函数,这个函数一方面可以通过View的requestLayout函数层层调用,另一方面在ViewRootImpl的setView中也会调用。函数中先判断mHandlingLayoutInLayoutRequest是不是false,如果是true的话,就意味着,当前正在对layout请求处理中,我们就不需要在额外的处理了。然后会调用checkThread()来进行线程的检查,

                                                   checkThread

       checkThread()函数里面的逻辑是检查当前的线程是不是mThread对应的线程,而mThread是在ViewRootImpl的构造函数里被初始化的。参考上一章我们知道mThread对应的是主线程,所以这也是为什么不能在子线程更新UI的原因。

       线程检查完之后,将mLayoutRequested设置为true,这个字段很重要,它是区分view的invalidate()和requestLayout()两个函数的标志,最后调用scheduleTraversals函数:

                                              scheduleTraversals

       scheduleTraversals先给主线程发送了一个屏障消息,将同步消息阻塞住,然后调用了mChoreographer.postCallback方法,这里涉及到了另一个非常重要的类:Choreographer。

       Choreographer,翻译过来是编舞,源码解释这个类的作用是对动画,输入和绘制的计时进行统一协调。听起来比较抽象,我尝试从自己的角度来解释一下。首先是垂直同步-Vsync,当我们的App在某个设备上运行之后,设备的屏幕会按照一定的频率不断的刷新屏幕,这是硬件决定的。每一次刷新屏幕,都会对外发送一个Vsyn信号。因为一个设备上会有很多个App,每个App都会有自己的动画或者绘制需求,所以就需要对这些杂乱无章的需求进行统一管理,按照显示屏的刷新频率,每隔一段时间统一的绘制,从而让他们的步调保持一致,而Choreographer实现了这一点,也就是Choreographer让我们的和绘制相关的需求和屏幕的刷新频率保持了一致。另一方面,从字面上来讲,编舞就是按照某种节奏来跳舞,而Choreographer就是让我们的和绘制相关的所有逻辑遵循屏幕的刷新频率。

       Choreographer内部又依赖于DisplayEventReceiver,DisplayEventReceiver是一个较为底层的函数,内部通过native函数实现了Vsync的信号的接收。具体过程是当需要响应Vsync信号时,先调用scheduleVsync函数:

       这样下一帧开始的时候,我们就可以接受到信号了,然后会回调onVsync函数,我们需要的是重写这个函数,添加我们的业务逻辑:

       而Choreographer也是依赖于DisplayEventReceiver来实现的,按照源码的解释,一般情况下,我们不需要使用Choreographer这个类,更多的是调用动画或者View的函数就可以了,它们内部会调用Choreographer,由此可见,动画也是依赖于Choreographer,让动画的刷新频率和屏幕的刷新频率保持一致。举个例子就是View的postOnAnimation:

                                              postOnAnimation

       在下一帧开始的时候,执行指定的Runable。对于Choreographer,它本身也是线程单例的,一个线程一个,通过ThreadLocal实现。并且这个线程必须要有Looper,因为Choreographer内部要依赖Handler消息机制,一般通过getInstance来获得对象:

                                                   getInstance

       而Choreographer最重要的两个函数是postCallBack和postFrameCallBack,这俩其实一样,只不过一个执行的是Runable,另一个执行的是FrameCallBack,而且都是在下一帧开始的时候执行。我们只分析postCallBack就好。而postCallback既可以马上执行,又可以延迟执行,最终还是调用下面的函数:

                                         postCallbackDelayedInternal

         参数里面的action可能是Runable,也可能是FrameCallback,然后将其存放在mCallBackQueues代表的数据结构中。对于这个数据结构,这里就不展开细讲了。放入CallbackQueue存储后,会被封装成CallBackRecord:

                                                  CallbackRecord

          其中的action,就是传入的Runable或者FrameCallback,另外一个token是用来区分action的。因为当使用的是FrameCallback的时候,postFrameCallback会给他添加一个FRAME_CALLBACK_TOKEN的token:

                                           postFrameCallbackDelayed

       所以在CallbackRecord内部就可以通过这个FRAME_CALLBACK_TOKEN来区分Runable或者FrameCallback。存储完成了之后,接下来就要根据执行时间来判断,需不需要马上执行。如果马上执行的话,直接调用scheduleFrameLocked。如果有延迟的话,就会通过Handler来发送消息,这样等时间到了之后,再来调用scheduleFrameLocked:

                                                  doScheduleCallback

          接下来再来看scheduleFrameLocked函数:

                                                    scheduleFrameLocked

       这里会先判断USE_VSYNC字段,这个字段是通过读取系统属性赋值的:

                                                      USE_VSYNC

       可以理解为能不能接收到vsync信号,如果USE_VSYNC为true的话,会先通过isRunningOnLooperThreadLocked来判断调用的线程和Choreographer绑定的线程是否一致,如果一致的话,直接调用scheduleVsyncLocked,不一致的话,则会通过mHandler来做线程切换:

                                                        FrameHandler

                                                       doScheduleVsync

       而scheduleVsyncLocked里面的逻辑就简单了,直接调用DisplayEventReceiver的scheduleVsync函数来进行注册,这样就可以接收到下一帧开始绘制时的vsync信号。当下一帧开始绘制的时候,Choreographer内部的FrameDisplayEventReceiver的onVsync函数会被调用:

                                                     onVsync

        这里只截取了关键的代码,在计算好下一帧的时间之后,直接发送一个Runable形式的Message,而这个Runable就是FrameDisplayEventReceiver自己,然后就会在run函数里调用doFrame函数:

                                                             doFrame

       在doFrame中首先根据时间做了一些判断逻辑,主要是来判断有没有抖动啊,跳帧啊或者延迟之类的。这个地方笔者并没有深入研究,所以这里就暂时先略过了。重点看下面的代码:

                                                         doFrame-2

       这个地方就依次对不同类型的Callback进行调用,前面我们分析到,Choreographer会将输入啊,动画啊还有绘制统一处理,而相应的在Choreographer内部就有了三个Callback Type:

                                                     Call back type

         其中,View显示用到的是CALLBACK_TRAVERSAL,这三种类型是有顺序的,会按照INPUT-ANIMATION-TRAVERSAL依次执行,具体执行的函数是doCallbacks:

                                                              doCallbacks

          这里也只截取了关键代码,由于刚才三种类型的Callback存储的时候会被封装成CallbackRecord,这里就直接调用它的run函数:

                                                 CallbackRecord$run()

       run函数内部通过token来区分FrameCallback和Runable,然后分别调用。以上,就是在USE_VSYNC==true的前提下的处理,如果USE_VSYNC==false的话,还是会依靠Handler来进行处理:

        这个时候会计算出下一帧的时间,然后通过Handler来发送消息:

       然后在handleMessage里面来调用doFrame,之后的逻辑就一样了。这里需要注意的是,Choreographer这个类是在4.1的系统之后才添加的,在此之前Android应该是直接依靠Handler和消息队列来处理相关事件,就像USE_VSYNC==false的时候一样,所以Looper对于Choreographer来说很重要,这也是Choreographer为什么一定要依赖一个带有Looper线程的原因。

       分析完Choreographer,我们继续回到ViewRootImpl中:

                                                scheduleTraversals

        那现在,这段代码我们就可以知道,它通过postCallback函数,传递了一个类型为CALLBACK_TRAVERSAL,由mTraversalRunnable代表的Runable对象:

                                                       TraversalRunnable

        它内部调用了doTraversal函数:

                                                       doTraversal

       而这个时候,我们会看到一个非常熟悉的函数performTraversals,然后在performTraversals开始了真正的显示流程。

二:performTraversals

       performTraversals这个函数很长,大约800行的代码,核心逻辑就是处理View显示的时候需要经历的测量,布局和绘制三个阶段,我们一点点的分析:

                                                  performTraversals-1

      首先,将mIsInTraversal和mWillDrawSoon两个字段设置为true。前者代表当前的ViewRootImpl正在Traversals中,后者表示接下来马上会执行绘制。其中,mWillDrawSoon这个字段后面还会用到,用来避免重复绘制。如果它等于true的话,意味着接下来马上也要执行绘制流程了,我们就没必要发送重复的请求了。同时,在performTraversals开始阶段就把mWillDrawSoon设置为true,也暗示了当performTraversals被调用的时候,绘制阶段肯定是会被执行的,但是测量和布局阶段则不一定。

       接下来会判断mFirst,这个字段true或者false主要是影响desiredWindowWidth和desiredWindowHeight这两个字段的值的获取。这两个字段看名字就知道,代表接下来显示的窗口的宽高,

                              desiredWindowWidth&desiredWindowHeight

       当desiredWindowWidth和desiredWindowHeight发生了变化的时候,就会把mFullRedrawNeeded和mLayoutRequested设置为true。前者为true的话,意味着窗口的显示区域要全部重绘,后者代表需要执行一次布局,也就是意味着要执行测量和布局阶段,

                                                     performTraversals-2

         接下来会声明一个名为layoutRequested的boolean字段,它的值可以简单的理解为mLayoutRequested,当layoutRequested==true的时候,会调用一个重要的函数:

                                                    measureHierarchy

        measureHierarchy,也就是对整个View Tree进行测量,从而开始了测量阶段。这个函数在performTraversals中不是一定会被调用的,它被调用的前提就是mLayoutRequested为true,而ViewRootImpl的requestLayout会把它设置为true。接下来我们来看measureHierarchy的逻辑:

                                                     measureHierarchy-2

        首先会判断WindowManager.LayoutParams的width是不是WRAP_CONTENT,对于Activity来说,我们知道不是,它是MATCH_PARENT,所以这部分跳过,继续向下看:

                                                   measureHierarchy-2

       接下来是调用了getRootMeasureSpec,也就涉及到了测量阶段一个非常重要的类MeasureSpec,也就是测量规格。这里先从概念的角度理解一下,所谓的测量规格,可以理解为ViewGroup对它的子View的大小的限制。一个MeasureSpec对象,包括模式和尺寸两部分,尺寸很好理解,就是具体的大小。而模式又分为了UNSPECIFIED,EXACTLY和AT_MOST,并且MeasureSpec提供的相应的拆装箱函数。这个地方通过使用int的位运算,避免了对象的分配,从而提高了效率。

       刚才是从概念的角度来分析了MeasureSpec,但理解起来还是比较枯燥,接下来我们会从代码的角度来分析,就很好理解了。在使用的过程中,MeasureSpec可以分为封装和解析两种情况,首先来看一下刚才的getRootMeasureSpec:

                                                  getRootMeasureSpec

       这个函数的作用是给当前View Tree的根View,也就是DecorView生成一个MeasureSpec对象,宽高各一个。在生成的时候,需要考虑两部分,一部分是LayoutParams,代表了DecorView自己的诉求,而另一部分就是windosSize,当前窗口的大小,正常情况下也就是屏幕的大小。如果LayoutParams是MATCH_PARENT,则意味着DecorView的意愿是想占据窗口所有的区域,那么就封装一个模式为EXACTLY,大小为屏幕大小的MeasureSpec对象。如果为WRAP_CONTENT,则意味着DecorView只想要一个能够把它的内容显示出来的尺寸就可以。但ViewRootImpl是不知道你具体需要多大的,只能给你一个上限,最大就是屏幕的尺寸,所以会封装一个模式为AT_MOST的MeasureSpec对象。如果既不是MATCH_PARENT,也不是WRAP_CONTENT,那就意味着LayoutParams里面指定的是一个具体的数值,这个时候ViewRootImpl就会直接使用这个数值,并把模式指定为EXACTLY。

        以上就是DecorView宽高的MeasureSpec生成的过程,但还没有结束,当DecorView接收到了MeasureSpec之后,还需要进行解析,这个后面会继续分析。那根据DecorView宽高MeasureSpec生成的过程,我们可以扩展一下ViewGroup给它的子View生成MeasureSpec的逻辑。ViewGroup在给子View生成MeasureSpec的时候需要考虑两方面的因素,一方面是自己的可用大小,需要考虑padding等因素,另一方面就是子View的LayoutParams里面宽高具体的值,同时也需要考虑margin的因素,然后才能正确确定子View的宽高。注意由于这里是测量阶段,只会考虑宽高,而不需要考虑子View改如何摆放,如何摆放是layout阶段操心的事。

       那通过刚才的getRootMeasureSpec之后,就给DecorView的宽高分别指定了一个大小为屏幕大小,模式为EXACTLY的MeasureSpec,然后就调用performMeasure函数:

                                                  performMeasure

      它里面直接调用了mView的measure函数,我们在来看一下measure函数的逻辑:

                                                         measure-1

       函数是被final修饰的,所以子类不可以重写。这个函数就是每个View对象测试阶段的开始,是被当前View所在的ViewGroup来调用的,View完成自己的测量或者对自己子View的测量,具体的计算逻辑要放在onMeasure中。

       对于measure函数里的逻辑,首先一个就是mMeasureCache,也就是测量缓存,View会把每次接收到的宽高的测量规格保存起来,如果后续接收到的测量规格被缓存过,那么就可以直接拿过来使用。

                                                      measure-2

       接下来会判断mPrivateFlags的PFLAG_FORCE_LAYOUT为有没有被设置,通过前面的分析,我们知道,在requestLayout函数中就已经被设置了。或者是接收到的测量规格和上次的测量规格是否一致,如果不一致或者PFLAG_FORCE_LAYOUT位被设置了,就会走if里面的代码。下一步会先把mPrivateFlags里面的PFLAG_MEASURED_DIMENSION_SET取消掉,因为View里面规定宽高的设置必须通过setMeasuredDimension来设置,然后在这个函数里会设置PFLAG_MEASURED_DIMENSION_SET这个位,并且measure后面会判断这个位有没有配置,没有的话会抛异常。

       然后会尝试获取一下cacheIndex,在PFLAG_FORCE_LAYOUT被设置的时候,cacheIndex是-1,没设置的话,就会调用indexOfKey来获取,没有缓存的话也是-1。如果在cachIndex<0或者sIgnoreMeasureCache=true的话,就会调用onMeasure来进行测量。这个sIgnoreMeasureCache的赋值根版本号有关,在4.4之前就是true:

                                               sIgnoreMeasureCache

        所以测量缓存可以理解为4.4之后才添加的优化手段,对于onMeasure,我们先来看一下View的默认实现:

                                                       onMeasure

       这里直接调用setMeasuredDimension来设置了宽高,而具体的尺寸通过getDefaultSize来获取:

                                                      getDefaultSize

       这个地方就涉及到了MeasureSpec的解析。针对于View的MeasureSpec解析,一方面接收到的MeasureSpec对象就已经考虑到了View自己的LayoutParams和ViewGroup的可用大小,另一方面View需要根据自己的绘制需求,来确定为了满足显示内容的需要,需要多大的尺寸,这个地方每个View根据自己的绘制需求来确定就好。由于从View的角度没办法确认自己需要多大的绘制区域,所以getDefaultSize函数里就使用getSuggestedMinimumWidth获得了一个最小的尺寸,用这个尺寸来作为View的需求。确认了自己需要的尺寸之后,就对MeasureSpec来进行解析,getDefaultSize的代码也很好理解。唯一的一个问题就在于对AT_MOST模式的处理上,View将它和EXACTLY模式当成一种情况来处理了,直接使用的是MeasureSpec里面的尺寸。根据前面的分析,我们知道当模式为AT_MOST的时候,意味着View在xml文件里声明的是wrap_content,而MeasureSpec里面的尺寸就是ViewGroup的可用大小,这么做就意味着wrap_content失去了作用,所以我们在自定义View的时候需要注意这种情况。

       刚才是对View的onMeasure逻辑进行了分析,接下来就是ViewGroup,ViewGroup自己并没有对onMeasure进行重写。对于ViewGroup的onMeasure逻辑,一方面要完成子View的测量,通过调用子View的measure函数来实现;另一方面,当所有的子View测量完毕了之后,其实就可以通过这些子View的大小来确定该ViewGroup自己的大小需求,也就是说,ViewGroup自己的测量需求就是将自己的子View显示出来就可以了,这里我们可以拿简单点的FrameLayout来举例:

                                             FrameLayout$onMeasure-1

       这个函数,首先判断当前FrameLayout宽高的测量规格是不是EXACTLY模式,如果有一个不是的话,那么measureMatchParentChildren会被设置成true。而这个字段的意义在于,如果不是EXACTLY模式,就意味着当前FrameLayout在xml文件中声明的是wrap_content,那么就会对子View中为match_parent的进行重新测量,这里先略过,后面在仔细分析。

       接下来就要对子View进行测量,默认规则是只对不是GONE的子View来测量,但可以通过设置mMeasureAllChildren字段来对所有的子View来测量:

                                                       setMeasureAllChildren

        而测量子View的具体逻辑放在了measureChildWithMargins中:

                                                  measureChildWithMargins

       这里需要注意的是,这个函数只会关注View的测量,而不会考虑布局,那是下一阶段需要考虑的事情,所以我们分析的时候不需要考虑View的位置。measureChildWithMargins里是通过调用getChildMeasureSpec函数来确定子View的MeasureSpec,这其中还需要考虑当前ViewGroup的不可用区域,也就是ViewGroup的padding和子View的Margin。然后我们继续看getChildMeasureSpec函数:

                                                     getChildMeasureSpec

       getChildMeasureSpec的实现和ViewRootImpl里面的getRootMeasureSpec大同小异,需要注意的有两个地方。一个是如果子View在xml文件里声明了具体的数值,这个优先级是比较高的,那么不管ViewGroup是什么测量模式,给子View的都是EXACTLY模式。另一个地方在于,当子View在xml文件里声明的是match_parent,一般得到的应该是EXACTLY模式。但如果ViewGroup自己是AT_MOST模式,那么即便子View声明了match_parent,得到的也是AT_MOST模式,这两个地方额外注意一下就可以。确定好了子View的MeasureSpec,然后调用measure函数开始测量就可以了。

       由于FrameLayout摆放子View的时候相当于相互遮盖的,所以我们只需要考虑子View中最大的宽高就可以了,这些被记录在了maxWidth和maxHeight字段里。然后又调用了combineMeasuredStates函数:

                                                  combineMeasuredStates

       这个函数的目的很简单,就是对两个measure state进行合并,那么接下来分析一下这个measure state。这里拿宽举例,View的宽被存储在mMeasuredWidth字段中,如果我们想获取的话,就得调用它的getMeasuredWidth函数:

                                                          getMeasuredWidth

       这里我们会发现,它会做一个位运算。其实mMeasuredWidth和MeasureSpec很类似,它里面也可以分为两部分,一部分是测量大小,而另一部分是测量状态,所以调用getMeasuredWidth的时候,得先进行位运算,才能得到真正的宽高。笔者看代码,感觉测量状态应该只有一个,就是MEASURED_STATE_TOO_SMALL,而它则是通过resolveSizeAndState封装进去的:

                                                      resolveSizeAndState

       那这里就可以清晰的看到,如果测量模式为AT_MOST,并且测量规格里的最大值满足不了我们的需求,这个时候就会标记MEASURED_STATE_TOO_SMALL状态。而其中的参数childMeasuredState代表的则是子View们的测量状态,而FrameLayout就是通过不断的调用combineMeasuredStates进行状态整合而得到的。

       完成了子View的测量,就在依次考虑了padding,suggestedMinimumHeight和前景图的minumWidth的情况下,对maxWidth来进行修正。最后先调用resolveSizeAndState来封装测量状态,在调用setMeasuredDimension给FrameLayout的宽高赋值。

       FrameLayout最后还会考虑mMatchParentChildren,这个List里面存放的是宽或高为match_parent的子View。在前面的measureMatchParentChildren==true的情况下,它里面是有元素的。之所以对这些子View重新测量,个人理解为就是强制性的将子View接收到的MeasureSpec的测量模式改为EXACTLY,因为默认情况下虽然子View声明了match_parent,但由于FrameLayout自己是wrap_content,所以子View接收到的还是AT_MOST。

                                              FrameLayout$onMeasure-2

       那么到这里,FrameLayout作为ViewGroup的代表,它的onMeasure逻辑已经分析完了,然后我们继续回到measure函数中,

                                                             measure

       经过了onMeasure的处理后,measure函数就会把mPrivateFlags中的PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT位取消掉。反之,如果cacheIndex>=0,就意味着我们是从缓存里读取来的数据,就会设置这个标志位。这个标志位代表我们跳过了onMeasure的逻辑,所以接下来在布局阶段中,会把这个流程补上。无论是走的onMeasure也好,还是读取的缓存也罢,总之我们都重新调用了setMeasuredDimension函数来给View重新设置了宽高,那么也就需要重新布局一次,所以会给mPrivateFlags设置PFLAG_LAYOUT_REQUIRED位,这个在后面的布局阶段中,会做判断。

                                                              measure

      在measure最后,会修改mOldWidthMeasureSpec和mOldHeightMeasureSpec这两个字段,并重新放入mMeasureCache中来缓存:

                                                                  measure

      关于mMeasureCache还有一点就是,当调用View的requestLayout函数的时候,会把它清掉。至此,我们完成了测量阶段的分析,但是这也只是显示流程的开始。测量完了之后,下一步就是布局,针对的是ViewGroup,需要ViewGroup将它的子view摆放好位置。那我们继续回到performTraversals,在进行布局之前,performTraversals里有一大段代码是针对surface的处理,这部分在这里就暂时跳过了,有兴趣的朋友可以看老罗的博客。我们直接来看布局里骨干的代码:

                                                  performTraversals

       可见,performTraversals是通过调用performLayout来开始布局阶段的,并且这个函数也不是一定被调用,只有layoutRequested==true,也就是mLayoutRequested==true的时候才会调用:

                                                           performLayout

       performLayout首先先把mLayoutRequested设置为乐false,这样后续的布局请求就可以正常处理了,然后调用了根View的layout函数:

                                                             layout

       对于View对象来说,布局阶段的开始就是layout函数被调用。和测量阶段类似,layout函数是布局阶段的开始,也是给ViewGroup用的,从而让ViewGroup通过这个函数来确定各个子View的位置,这个时候就得需要用到测量阶段计算出来的宽高。对于DecorView,由于是占满屏幕,所以ViewRootImpl直接调用了host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight())来摆放DecorView的位置。

       在layout函数中,首先判断mPrivateFlags中的PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT位有没有被配置。这个标识位,我们刚在measure函数里看到,当onMeasure被跳过的时候,就会设置这个标志位。如果配置了,就会重新调用一次onMeasure函数,再测量一次。layout函数中接下来继续调用setFrame函数,来真正的对位置相关的字段进行修改:

                                                                  setFrame

       setFrame会返回一个boolean值,如果返回true的话,就代表了当前View的位置或者大小发生了变化,这个返回值由changed指定。具体判断逻辑是:只要mLeft,mRight,mTop和mBottom这四个字段的值,任何一个发生了变化,就会返回true。当View的位置或者大小改变了之后,就会走进if里面的代码。首先会从mPrivateFlags中获取PFLAG_DRAWN位的值,这个标志位被配置了,就代表View上一次的绘制结束了,可以开始一次新的绘制,这里大家先有一个印象。下一步,layout又调用了invalidate函数。对于这个函数,大家都很熟悉,当View的显示内容失效了,就可以调用这个函数,从而在View的可见性不是Gone的情况下,可以进行重绘。这个地方之所以调用invalidate,是因为这个时候,View的位置四个字段的值还没有真正的改变,所以本意是让View在当前位置以当前大小在显示一次,从而接下来位置改变之后,可以平滑的过度,这个地方是参考老罗的解释。当然invalidate内部还会对PFLAG_DRAWN进行检查,如果上一次的绘制没有完成,那么这次的请求就会被忽略了。对于invalidate函数的流程,后面绘制的时候会有详细的解释。调用完了invalidate函数之后,才会对位置的四个字段的值进行更改,并且设置mPrivateFlags的PFLAG_HAS_BOUNDS位,表明当前View有边界了。如果大小发生了变化,还会调用View的onSizeChanged函数。

                                                              layout

       layout接下来会判断View是否是VISIBLE,如果是的话,就强制设置PFLAG_DRAWN,然后调用invalidate函数。这么做的目的是,因为当前View的位置已经改变了,所以就需要调用invalidate函数进行重绘。但这个时候就会有一个疑问,因为现在处于显示流程的布局阶段,接下来马上就是绘制阶段了,绘制的时候会直接应用到最新的位置,根本不需要调用invalidate多此一举啊,这个问题的原因个人猜测是layout可能不是在正常的显示流程中调用的。

       因为View的layout函数是public的,就意味着可以在View之外的任何地方调用,而且layout函数,也是实现View拖动的方式之一。View的拖动在实际开发中,也是很频繁的一个需求,可以使用的方式会在本文章中穿插的介绍。所以完全有可能,代码通过调用layout函数来改变View的位置,从而实现View的拖动,那么这时候就是在脱离了显示流程之外的地方调用。如果只是改变了View的位置字段的值,而没有重绘,这样的改变是没效果的。为了兼容这种情况,就需要在setFrame中调用invalidate,从而让View可以依据最新的位置来重绘。

                                                          setFrame

      setFrame最后,会把之前保存的PFLAG_DRAWN位的值,重新赋值回去。这个地方老罗给的解释是,这个地方invalidate的调用,应该是一个额外的中间阶段,在正常的显示流程之外添加的一个额外的刷新,它不应该改变View原有的显示流程,所以需要复原。那分析完了setFrame,回到layout中。

                                                                layout

       这个地方会进行判断,如果刚才setFrame返回的值位true或者mPrivateFlags配置了PFLAG_LAYOUT_REQUIRED位,就会走if里面的代码。而PFLAG_LAYOUT_REQUIRED这个字段我们也不陌生,在上面的测量阶段会被配置。在满足以上两个条件之一的情况下,layout就会调用onLayout。这个onLayout的调用对View没作用,如果是ViewGroup的话,就会通过这个函数重新摆放子View 的位置,而且摆放的时候就要充分考虑ViewGroup自己的摆放规则,子View的LayoutParams和子View测量宽高。等layout执行完毕了,PFLAG_FORCE_LAYOUT位的值也就被设为0了。

       相比较而言,布局阶段可能会简单一些。布局阶段这是ViewGroup的专利,View不需要考虑,只有ViewGroup才会需要通过这个阶段来确定各个子View的位置。布局完成了之后,下一阶段是绘制。由于篇幅限制,绘制阶段的内容放在下一节继续分析。