View原理细节(二)-显示流程

       本节我们来分析一下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的位置。

       那现在针对View的显示,测量阶段确认了大小尺寸,布局阶段确认了View的位置,接下来就是绘制了。根据前面的分析,Surface给我们提供了一个Canvas,绘制的时候就可以通过canvas的Api来绘制纹理。对于同一个View Tree来说,里面所有的View对象使用的是同一个Canvas对象,而像View的大小啊,位置啊这些信息,到现在还只是代码的字段,还没有被应用,用户根本感觉不到,最终都是让View依靠自己的大小和位置信息,对Canvas进行相应的移动和裁剪,然后在绘制,最终绘制出来的东西和它的位置和大小相匹配。并且在接下来对绘制流程的分析中,我们会发现,有很多实现了View的拖动的方式,其原理都是对canvas进行坐标系的移动,从而实现了拖动,这和前面的layout是不一样的,因为layout实实在在的改变了View的位置,位置字段的值也被改变了。

       让我们继续回到performTraversals函数中,来看一下绘制的流程,代码如下:

performTraversals

      在绘制之前,首先会涉及到一个类ViewTreeObserver。这是个普通的类,而不是接口:

ViewTreeObserver

        ViewTreeObserver这个类像一个工具,我们可以通过它注册一些监听,从而对某个View Tree的全局事件进行监听,常见的监听接口都定义在了ViewTreeObserver内部,比如OnGlobalLayoutListener,OnPreDrawListener和OnDrawListener等等。ViewTreeObserver需要注意的是,我们没办法像一个普通的类一样构造它的对象,然后设置给某个View Tree。这是ViewRootImpl的工作,一个View Tree对应一个ViewTreeObserver,ViewRootImpl已经配置好了,而我们如果想使用这个类的话,可以通过调用view的getViewTreeObserver来获取:

getViewTreeObserver

       这个函数会返回当前View所在的View Tree的ViewTreeObserver对象,但是这个对象不会保证长期存活,所以在调用它的函数之前,先通过isAlive来判断是否存活,然后在调用函数。如果在没存活的情况下,调用函数会抛出异常。

        在ViewTreeObserver所有的监听接口中,比较常用的应该是OnGlobalLayoutListener。一般都是通过这种方式可以得到某个View的宽高,因为一旦它被调用,就意味着View Tree的布局阶段已经结束了,是可以得到宽高的:

dispatchOnGlobalLayout

       分析完了ViewTreeObserver,回过头来继续看绘制。绘制的入口是performDraw,而这个函数执行的前提是cancelDraw和newSurface都是false。其中cancelDraw来源于ViewTreeObserver,意味着ViewTreeObserver里面的OnPreDrawListener没有想取消这次绘制的,而newSurface代表的是有没有给当前的Window重新构造一个绘图表面,这个地方具体的细节可以参考老罗的文章。

       cancelDraw和newSurface其中有一个为true的话,就要取消这次绘制,下一帧的时候在绘制,所以马上就会继续调用scheduleTraversals函数。注意这个地方重新进行的只是绘制阶段,而测量和布局阶段不会重新计算,因为调用的是scheduleTraversals,而不是requestLayout,mLayouteRequested字段==false。

       在可以绘制的前提下,就会调用performDraw来开启绘制阶段:

performDraw

performDraw又调用了draw函数:

draw-1

        其中参数fullRedrawNeeded代表的是,是不是需要全部区域重绘。这个参数个人暂时还没有考虑全所有的情况,只是大概的了解到在窗口滚动/动画,窗口尺寸发生变化和Surface重新构建了这些情况下,会被赋值为true,后续在陆续补充吧。接下来,我们略过Window层面的缩放啊,scroll啊这些信息,继续往下看:

draw-2

        mDirty代表的是当前View Tree的脏区域,也就是需要重绘的区域,在fullRedrawNeeded==true的情况下,就会把它设置为窗口的大小。我们这里先不考虑硬件加速的情况,那么接下来就会调用drawSoftware函数。调用完drawSoftware之后,如果当前窗口处于类似于滚动啊这样变化的情况,那么马上就会调用scheduleTraversals函数,从而在下一帧的时候继续绘制:

        刚才忽略的一点是,draw(boolean fullRedrawNeeded)函数中在调用drawSoftware()之前,会调用ViewTreeObserver的dispatchOnDraw()函数,而这个函数会调用在ViewTreeObserver上注册的OnDrawListener。

dispatchDraw()

       至于OnDrawListener,笔者暂时还没想到。猜测应该是类似于切面,做监听啊,数据统计之类的逻辑。继续来看drawSoftware()函数:

drawSoftware-1

         drawSoftware首先调用Surface的lockCanvas函数来得到一个Canvas对象,而这个Canvas对象就是当前View Tree所有View使用的Canvas。前面我们提到,View的显示包含两部分,一部分是App这一侧向图形缓存区里填充数据,而另一方面就是SurfaceFlinger对缓冲区里的数据进行渲染,然后用户就可以看到显示的内容了。Surface可以简单的理解为Framework给我们的缓冲区,会给我们提供一个Canvas对象。我们通过Canvas的Api来向Surface里填充数据,绘制完毕后,最后再调用Surface的unlockCanvasAndPost来进行渲染。

        调用lockCanvas的时候,会传入dirty,类型为Rect,也就是前面提到的脏区域。这里有个细节就是当lockCanvas执行完毕了之后,有可能会对dirty进行更改,那么mIgnoreDirtyState就会被设置为true了,这个字段后面会影响View的绘制流程,这里先注意一下。

drawSoftware

       得到了Canvas对象之后,就会把DecorView的mPrivateFlags的PFLAG_DRAWN设置为1,因为接下来马上就要绘制了,所以这里就可以接收新的绘制请求了,然后直接调用了DecorView的draw函数:

draw()-1

       对于某个View对象的绘制来说,draw函数可以理解为一个调度函数,里面设计好了当前View需要绘制的东西,并且列好了顺序。draw()函数中首先判断当前View的mPrivateFlags有没有配置PFLAG_DIRTY_OPAQUE位。如果设置了这个标记位,代表当前的View来自于一个不透明的请求。这里先简单介绍一下,后面的流程会具体的分析。这个标志位基本上都是应用在ViewgGroup上的,代表了它的子View不透明,那么这个时候该ViewGroup就不需要来绘制自己和背景了,就会跳过drawBackground()和onDraw()函数的调用。因为子View不透明,就会把当前的ViewGroup覆盖,这个ViewGroup就已经没有绘制的必要了,也算是一种优化吧。

draw()-2

        这个标记位具体的设置,会在后面的invalidateChid()函数里面看到。标记判断完了之后,会给当前的View设置PFLAG_DRAWN位。这个标记的设置很重要,它是我们可以通过invalidate来不断刷新View的重要前提之一。在draw()函数正常的绘制流程中,第一步是绘制View的背景,由drawBackground()函数负责:

drawBackground()

       这个函数里,唯一需要注意的是,在绘制背景之前,会先消除scrollx和scrolly的影响。针对scrollx和scrolly大家都很熟悉,这是Android Framework给我们提供的可以实现View拖动的方式之一。它的原理就是对canvas进行移动,从而移动View的绘制内容。Android帮我们做好了一切,随便一个自定义View,即便我们不作任何处理,也可以通过这种方式来实现View的拖动。它移动的是View的绘制内容,但对背景的绘制没有影响,所以绘制背景之前,会先消除scroll所带来的影响。这是除了setLayoutParams和layout()之外,第三种可以实现View拖动的方式。

        绘制完了背景之后,接下来会通过onDraw来绘制自己。这个就没有一定之规了,各种View根据自己的需求来绘制,直接略过。绘制完了自己,就需要通过dispatchDraw来绘制子View了。这个函数在View里是空实现,我们直接看ViewGroup里面的逻辑。

dispatchDraw()-1

        dispatchDraw()函数里面的逻辑也很长,这里我们略过了LayoutAnimation的情况,考虑最简单的情况下的实现逻辑。首先来判断当前ViewGroup有没有配置CLIP_TO_PADDING_MASK。这个标记位的意思是根据当前ViewGroup的padding来对canvas进行裁剪。这个是默认被设置的,所以ViewGroup在dispatchDraw的时候,会考虑自己的padding和scrollx/y来对canvas进行裁剪。裁剪完了之后,就对子View进行遍历,挨个调用drawChild来完成各个子View的绘制。而drawChild内部调用的是子View的draw(Canvas canvas, ViewGroup parent, long drawingTime):

draw(Canvas canvas, ViewGroup parent, long drawingTime)

        那至此,我们可以得出一个结论:对于DecorView来说,它绘制的起点是draw(Canvas canvas)函数,但对于其他的View来说,绘制的起点是draw(Canvas canvas, ViewGroup parent, long drawingTime),因为每个View的绘制是在所在ViewGroup的dispatchDraw里进行的,而dispatchDraw里面调用的则是子View的draw(Canvas canvas, ViewGroup parent, long drawingTime)函数。接下来看draw(Canvas canvas, ViewGroup parent, long drawingTime)函数的实现逻辑:

draw(Canvas canvas, ViewGroup parent, long drawingTime)-1

       对于绘制,这里不考虑硬件加速,也不考虑动画,就在最普通的情况下来分析绘制的流程。第一个需要注意的地方是hasIdentityMatrix()函数,当View正在进行属性动画的时候,这个地方返回的是false,正常的情况下返回的是true。对于属性动画,大家可以去官网或者扔物线大神的文章。这里之所以提起属性动画,是因为属性动画也是实现View拖动的一个方式。这里不展开来细说,只从View拖动的角度,只关注translationX/Y属性。hasIdentityMatrix()函数的返回值会被赋值给childHasIdentityMatrix字段,有动画的话就是false,正常情况下就是true。

draw(Canvas canvas, ViewGroup parent, long drawingTime)-2

         接下来会给当前的View设置PFLAG_DRAWN位,然后会根据情况来调用canvas的quickReject。这个函数的目的是为了跳过一些不必要的绘制,比如在某个ViewTree中,某个View调用了invalidate函数,如果它的直系父View有个兄弟View,并且没有任何交集,那么这个兄弟View会通过这个函数来直接跳过绘制。但其实这个函数的原理笔者也不是特别明白,它是native函数,只是进行了代码验证,得到了这个结论。

       在不考虑硬件加速,并且Layer Type为LAYER_TYPE_NONE的情况下,很多代码都可以略过。

draw(Canvas canvas, ViewGroup parent, long drawingTime)

       这个地方调用了computeScroll函数,并获取了mScrollX和mScrollY,熟悉scrollTo的朋友对这个地方会很熟悉。前面也提到了这是View拖动的方式之一,原理是移动canvas。

draw(Canvas canvas, ViewGroup parent, long drawingTime)

        在不考虑硬件加速的前提下,drawingWithDrawingCache字段为false,offsetForScroll为true。所以这里首先调用了canvas的save函数,保存了当前的图层,从而确保当前View对Canvas堆栈状态的修改不会影响其他的View,然后就调用了translate函数。translate函数也很好理解,移动canvas,同时坐标系也会跟着移动,而且参数值的正副和坐标系的方向是一致的。这里我们从y轴方向来分析,它考虑mTop和mScrollY两个字段。mTop字段很好理解,这是布局阶段的计算结果,代表了当前View在所在的ViewGroup上顶点的位置,并且这个字段本身就已经考虑了ViewGroup的padding和View自己的margin。我们根据mTop来移动canvas的位置,从而让canvas的绘制区域和View的位置保持一致,这没问题。然后在考虑mScrooY,这个字段的定义指的是y轴方向上,当前View的显示内容相对于它的位置移动的距离:

mScrollY

       View在y轴的位置由mTop代表,要想单纯的移动显示内容,其实也就是移动canvas的坐标系,从而让它坐标系起点和mScrollY保持一致。再由于mScrollY的正负和坐标系的方向相反,比如当mScrollY==10的时候,就意味着canvas要向上移动10个像素,所以translate使用的时候用的是-sy。

       通过这个地方的分析,我们可以意识到,上个布局阶段的计算结果在这里发挥了作用,绘制阶段遵循了布局阶段的计算结果,从而让canvas 的绘制区域和View的位置区域保持了一致。同时,还会考虑了mScrollX和mScrollY的影响。所以,Scroller机制就是系统提供给我们的实现View滑动的一种方式,View本身自己做好了一切,即便是自定义的View,也不需要做任何额外的处理,只需要调用scrollTo函数就好。并且这种方式确认没有改变View的位置,因为mTop等字段的值并没有被改变,只是改变了canvas的坐标系起点,从而影响了显示区域的位置。那这里有个问题就是,DecroView的draw(Canvas canvas, ViewGroup parent, long drawingTime)函数是没有被调用的,是直接调用的draw(Canvas)函数,因为DecorView的Scroll逻辑在ViewRootImpl里,被当作Window级别的因素被处理了,所以可以直接调用draw函数。

        刚才明确了View的位置字段和mScrollX对Canvas坐标系的影响,从而影响了View的显示内容的区域,继续往下看:

draw(Canvas canvas, ViewGroup parent, long drawingTime)

        这个地方的if有四个判断条件,我们这里只需要关注hasIdentityMatrix(),当hasIdentityMatrix函数返回false的时候意味着当前的View有一个动画矩阵。这里我们以属性动画的translationY为例来,来看一下setTranslationY()函数:

setTranslationY

       通过源码,我们发现setTranslationY内部调用了mRenderNode的setTranslationY函数,mRenderNode的类型为RenderNode。要想完全理解RenderNode,就要了解硬件加速。由于本文的分析,是不考虑硬件加速的情况的,而且笔者对于硬件加速的知识点也没有完全的研究透,所以就简单的介绍一下。要想理解硬件加速,就离不开DisplayList,在使用硬件加速绘制的时候,会把View的绘制内容拆分成一个个的Display list,这样做的好处就是,如果只是View的一小部分需要刷新,那么就只需要修改这部分对应的DisplayList就好,其他的不需要改变,从而突破了传统绘制的瓶颈,提高了效率。而Display List可以理解为包含在RenderNode中,在RenderNode中,除了有DisplayList之外,还有其他的一些属性,比如scaleX,translationY等,这些可以理解为Display List对应的DisplayListCanvas的属性,不会影响绘制的纹理,这部分由DisplayList来负责。所以可以做到在绘制内容不变的情况下,直接改变属性,比如改变translationY,就可以在不对显示内容重绘的情况下,从而实现显示内容的移动,提高了绘制的效率。

       虽然我们这里并没有依赖硬件加速,但是针对于View的属性动画,View内部直接依赖了RenderNode来实现。所以当View的setTranslationY函数被调用的时候,它内部直接调用了RenderNode的setTranslationY函数。除了修改RenderNode之外,setTranslationY内部还调用了invalidateProperty()函数:

invalidateViewProperty

       在不考虑硬件加速的情况下,isHardwareAccelerated()就会返回false,所以会走if里面的逻辑。首先在invalidateParent==true的情况下,调用invalidateParentCaches()函数,这个函数内部只是给所在的ViewGroup的mPrivateFlags设置了PFLAG_INVALIDATED位。这个函数我们可以忽略,因为个人理解它只会在硬件加速的情况下才会发生作用。接下来在调用invalidate()函数,并根据第二个参数forceRedraw来决定要不要强制的设置mPrivateFlags的PFLAG_DRAWN位。因为在setTranslationY()函数中,invalidateProperty()函数被调用了两次。第一次调用的时候,translationY属性还没有被改变,所以这次的调用,forceRedraw参数为false。个人猜测和前面的layout()的逻辑一样,会尽量的让View在当前的属性下显示一次,从而实现平稳的过度,但不会打乱原有的绘制逻辑,所以forceRedraw==false,不强制打乱原有的绘制逻辑。等第二次调用的时候,translationY属性已经被改变了。就要强制刷新了,所以forceRedraw设置为true,从而强制设置mPrivateFlags的PFLAG_DRAWN标志位。

     当View的setTranslationY函数被调用了之后,它的hasIdentityMatrix()也就会返回false了,那么回到draw(Canvas canvas, ViewGroup parent, long drawingTime)函数中,就会执行下面的代码:

draw(Canvas canvas, ViewGroup parent, long drawingTime)

       这个地方就会让canvas结合getMatrix()返回到Matrix,而getMatrix()返回的就是RenderNaode的matrix:

getMatrix()

      当我们调用了View的setTranslationY函数的时候,RenderNode的Matrix就会改变,从而我们可以确定,通过setTranslationY的方式来实现View的滑动,本质上也是改变Canvas的位置,而View位置字段的值并没有改变。针对于View的滑动,另一个常见的函数是offsetTopAndBottom():

offsetTopAndBottom()

       这个函数的方式就真的会改变View的位置了,因为它的mTop和mBottom字段的值会被改变。一方面改变了mTop和mBottom两个字段的值,另一方面通过ViewParent的invalidateChild函数进行了draw pass,从而让当前的函数重绘。重绘的时候就会使用View最新的位置字段对Canvas进行移动和裁剪,从而实现了View的滑动。关于invalidateChild的细节,后面会和invalidate()函数一起分析。

        刚才的translationx/y和scrollx/y都不会改变View的位置,都只是改变canvas的位置,那它们两个的区别在哪呢,区别就在于View背景的绘制上。先继续往下看draw(Canvas canvas, ViewGroup parent, long drawingTime):

draw(Canvas canvas, ViewGroup parent, long drawingTime)

       接下来就要对canvas进行裁剪,刚才在考虑了mLeft,scrollx和translationx之后,已经对canvas进行了移动,改变了坐标系。但还是要裁剪,而裁剪的目的就是让canvas的显示区域和View的位置信息保持一致,也就是和mLeft为首的四个位置字段所构成的矩形区域保持一致,所以就会调用canvas的clipRect函数。clipRect函数只会裁剪,但不会影响坐标系。

       调用clipRect的前提,要求所在的ViewGroup的mGroupFlags被设置了FLAG_CLIP_CHILDREN位,而这个标志位是被默认设置的:

initViewGroup

         这里从y轴的角度来分析,clipRect的参数位sy,而sy的值就是mScrollY。这个地方笔者写了一个简图,来解释一下这个问题:

canvas裁剪

         图中有个矩形,可以理解为ViewGroup,最上边是A,最下边是B。这个ViewGroup里有个子View,它的y轴位置是从C到D,也就是mTop=C,mBottom=D。C距离A有两个间隔,假设距离为10,同时mScrolly也是10,也就是mTop和mScrollY都=10。当这个View的draw(Canvas canvas, ViewGroup parent, long drawingTime)被执行的时候,首先根据mTop和mScrollY来对View的canvas进行移动,最终子View坐标系的原点就来到了A,而它位置的上顶点位置mTop还是在在C,但是接下来裁剪的时候,又使用到了mScrollY。由于此时View的坐标系起点在A,clipRect也不影响坐标系,最终结果就是View的canvas的显示区域和它的位置保持了一致,是从C到D,但是坐标系的起点却在A,所以从起点绘制的话,就会有一种上移的效果。这个也正好体现了mScrollY只会影响显示内容不会影响View的位置。

        刚才的图确实有点糙,由于时间原因,笔者没找到好的画图工具,而且个人也更喜欢用文字来描述,所以敬请见谅。但这样的clipRect逻辑有个问题就是,它考虑了mScrollY,但是没考虑mTranslationY。一般同一个View的mScrollY和mTranslationY只会有一个被设置。由于scrolly的值和坐标系相反,为了和刚才的场景匹配,这里我们假设如果mScrollY=0,mTranslationY=-10,那会是一个什么结果呢。

       根据前面的分析,translationY也不会影响View的位置,位置字段的值不会被改变。在刚开始对canvas进行移动的时候,通过canvas的concat()考虑到了translationy,会把子View的坐标原点移动到A,此时子view的mTop的位置还是在c。但是clipRect的时候确没有考虑到translationy,只考虑了scrollY。那这个时候scrollY==0,然后从A裁剪,那么最终这个子View的显示区域就是从A到C的下面那个格。而且通过前面draw()函数分析的时候,我们知道背景绘制会消除scrollY的影响,但不会消除translationY的影响。所以translationY,相当于将子View整体向上移动了10个像素,无论是背景还是显示内容。这也是translationY和scrolly的区别,简单来说,scrolly的值,会改变View坐标系的起点,但是它的显示区域还是和位置保持一致的,只不过坐标系的原点已经不是显示区域的起点了,只会影响内容,不会影响背景。而translationY相当于整体移动View的显示区域,背景也会受到影响,最终View的显示区域和位置区域就不一致了。两者的相同点在于,View的四个位置坐标都没有改变。

         那再来分析一下,ViewGroup的scrolly对子View有什么影响。首先,ViewGroup中的draw(Canvas canvas, ViewGroup parent, long drawingTime)就会根据自己的scrolly对坐标系进行移动,然后前面的dispatchDraw()已经分析到,ViewGroup在传递canvas之前,就会根据自己的坐标系,并考虑自己的padding和scrolly的前提下,对canvas进行裁剪。而到了子View中,又会根据自己的顶点坐标来移动canvas,并且这个时候的移动就已经考虑到了ViewGroup的scrolly和padding的影响。所以结论就是改变一个ViewGroup的scrolly对于其中的子View来说,就相当于改变了它的translationy。


draw(Canvas canvas, ViewGroup parent, long drawingTime)

         回来继续看draw(Canvas canvas, ViewGroup parent, long drawingTime),接下来,会根据View的mPrivateFlags有没有配置PFLAG_SKIP_DRAW位,来决定是执行dispatchDraw或者draw。对于PFLAG_SKIP_DRAW位,首先我们先来看setWillNotDraw函数:

setWillNotDraw

       这个函数大家应该很熟悉,耳熟能详的一个优化的函数。如果当前的View不需要绘制,那么就可以调用这个函数,从而给当前的View设置WILL_NOT_DRAW位,并且后续Framework会有一定的优化。这个函数一般View不会调用,而ViewGroup默认调用了这个函数:

ViewGroup$WILL_NOT_DRAW

       因为ViewGroup一般不需要绘制自己,那么另一个问题就是,针对这个WILL_NOT_DRAW标志位,相应的优化体现在哪里呢,答案就是在PFLAG_SKIP_DRAW。setWillNotDraw带来的优化最终要通过PFLAG_SKIP_DRAW来体现,再来看一下刚才draw(Canvas canvas, ViewGroup parent, long drawingTime)函数的代码:

       如果View的mPrivateFlags位设置了PFLAG_SKIP_DRAW位,就会直接调用dispatchDraw,从而跳过View自己和背景的绘制,直接开始子View的绘制,这就是刚才所说的优化。而PFLAG_SKIP_DRAW位被设置的前提一是刚才的WILL_NOT_DRAW,另一方面还要求当前View不可以有背景和前景。在调用setWillNotDraw的时候,如果没有背景和前景,那么PFLAG_SKIP_DRAW就直接被设置了:

setFlags

        同时,在View的setBackground里,如果背景和前景都为null,同时又设置了WILL_NOT_DRAW位,那么PFLAG_SKIP_DRAW会被直接设置:

setBackground

        所以,假如在某个View Tree里面,某个View调用了invalidate,并且它的ViewGroup背景和前景都为null,那这个ViewGroup的绘制会被直接跳过,直接调用dispatchDraw,从而优化了流程。针对ViewGroup的优化一个是PFLAG_SKIP_DRAW,而另外一个就是刚才所提到的draw()函数里面的PFLAG_DIRTY_OPAQUE。也就是说,即便当前ViewGroup有背景,设置不了PFLAG_SKIP_DRAW,但如果当前ViewGroup来自于一个不透明的invalidate请求,那么它的绘制流程依然会得到优化。

        至此,View的draw(Canvas canvas, ViewGroup parent, long drawingTime)函数我们就分析的差不多,在这个函数里,一方面依据当前View的位置,Scrollx和translationX对canvas的坐标系进行了移动,从而影响了View绘制内容的位置区域,另一方面考虑了scrollx和scrolly,并且不改变View大小的前提下对canvas进行裁剪,从而让canvas的显示区域和View的位置保持了一致。

      针对任何一个ViewTree,它里面所有的View使用的是同一个Canvas,然后这个canvas在其中不断的传递。在传递的过程中,会受到View的位置,scrolly和translationy的影响,对canvas进行不断的变化,进而影响canvas的坐标系和显示区域,进而限制View的绘制范围。这样,View最终呈现出来的东西才会和用户期望的一致。

三:invalidate()

        针对View的显示流程,最后一个细节就是invalidate。这个函数大家都很熟悉,在动画啊或者自定义View中都会经常用到。一般理解这个函数的意思就是标志着当前View的显示内容失效,然后接下来会导致View的重绘。通过对前面View显示流程测量,布局和绘制三个阶段的分析,invalidate的学习就很简单了:

invalidate()

      在View中,也有好几个版本的invalidate,一般我们都是调用invalidate(),然后逐步调用到invalidateInternal():

invalidateInternal()

       invalidateInternal()内部首先通过skipInvalidate来判断是否可以跳过这次刷新,判断的逻辑也比较简单。如果既不可见,也没有进行动画,那么就可以跳过了。接下来会进行一系列的判断,其中最重要的一个条件就是PFLAG_DRAWN。如果当前View设置了PFLAG_DRAWN,并且也设置了PFLAG_HAS_BOUNDS,那么就可以刷新。刷新的话也是层层递进,从当前的View一直传到ViewRootImpl,这些操作由invalidateChild来完成,定义在ViewGroup中:

invalidateChild-1

      invalidateChild中第一个关键点是根据子View是不是不透明的来配置PFLAG_DIRTY_OPAQUE,这就是前面draw()函数里面提到的针对ViewGroup的优化。其中,子View是不是不透明通过它的isOpaque()来判断。只要isOpaque()返回true,那么就代表是一个不透明的请求。笔者随便写了一个View的子类进行测试,就是一个普通的View的子类,没有添加任何逻辑。这时候isOpaque()默认会返回true,当它的invalidate()被调用的时候,即便没有把所在的ViewGroup完全覆盖,所在ViewGroup的PFLAG_DIRTY_OPAQUE也会被设置,从而当ViewGroup的draw()函数被调用的时候,会跳过drawBackground()和onDraw()函数的调用,直接调用dispatchDraw()进行传递。

       第二个关键点就是通过循环,不断的向上调用invalidateChildInParent,从而实现了invalidate事件的向上传递。在向上传递的过程中,也会不断的给更高层的ViewGroup来设置PFLAG_DIRTY_OPAQUE。除此之外,invalidateChildInParent中最重要的一点,就是不断的根据当前View 的坐标系,来更改dirty,也就是这次的脏区域:

invalidateChildInParent

       层层传递,最终会到ViewRootImpl里面的invalidateChildInParent函数里:

invalidateChildInParent

       invalidateChildInParent会考虑窗口级别的mCurScrollY和mTranslator,来对dirty进行修正,然后在调用invalidateRectOnScreen():

invalidateRectOnScreen

         invalidateRectOnScreen先是把这次的dirty和已有dirty进行union,接下来就在mWillDrawSoon==false的前提下调用scheduleTraversals,而后面的事情我们就很熟悉了。

       至此,View的显示流程的细节已经分析完了。这部分细节的分析也花了笔者很多的时间,因为平时还得上班,只能利用业余时间来断断续续的完成,也会有其他的事情耽搁。这期间也是学了忘,忘了学,反反复复的好几遍。这篇文章算是个人总结的笔记,整体来说,感觉内容比较勉强。因为其中的东西,细节太多了,这篇文章只能在掌握整体流程的前提下,尽可能的分析更多的细节。但是个人感觉暂时还没有完全吃透,彻底的理解,慢慢的等以后,在加深理解,慢慢的领悟吧。

参考文章:https://blog.csdn.net/Luoshengyang/article/details/8372924

你可能感兴趣的:(View原理细节(二)-显示流程)