前言
在Android中View一直扮演着一个很重要的角色,它是我们开发中视觉的呈现,我平常也使用着Android提供的丰富且功能强大的控件,有时候遇到一个很炫酷的自定义View的开源库,我们也是拿来主义,时间长了你就会发现你只是一个只会使用控件和依赖被人开源库的程序员,这并不是一个开发者,所以我们并不能只满足于使用,我们要理解它背后的工作原理和流程,这样才能自己做出一个属于自己的控件,一直都说自定View是Android进阶中的一道门槛,当其实自定义View当你理解了它的原理后,你就会发现它也不过如此。本文将从源码的角度探讨View工作的三大流程,对View做进一步的认识。俗话说的好:源码才是最好的老师。
本文代码基于Android8.0,相关源码位置如下:
frameworks/base/core/java/android/*.java(*代表View, ViewGroup, ViewRootImpl)
frameworks/base/core/java/android/FrameLayout.java
View何时开始绘制?- requestLayout()
提到View,就不得不讲起Window,在Window,WindowManager和WindowManagerService之间的关系文章中讲过,Widnow是View得载体,在ViewRootImpl的setView方法中添加Winodw到WMS之前,会先调用requestLayout绘制整颗View Hierarchy的绘制,如下:
所以我们先从requestLayout()中看起,该方法如下:
1 | //ViewRootImpl.java |
requestLayout()中首先会检查线程的合法性,Android规定必须在主线程中操作UI,那么为什么不能在子线程中访问UI呢?这是因为Android的UI控件都不是线程安全的,如果在多线程环境下并发访问控件会导致控件处于不可预测状态。接着我们来看注释1,调用了ViewRootImpl的scheduleTraversals方法,如下:
1 | //ViewRootImpl.java |
在Android4.1之前Android的UI流畅性很差,所以在Android4.1之后引入了Choreographer机制和Vsync机制用来解决这个问题,Choreographer管理者动画、输入和绘制的时机,Vsync叫Vertical Synchronization(垂直同步)信号,每隔 16ms Choreographer就会收到来自native层的Vsync信号,这时Choreographer就会根据事件类型进行相应的回调操作,Choreographer支持4种事件类型回调:输入(CALLBACK_INPUT)、绘制(CALLBACK_TRAVERSAL)、动画(CALLBACK_ANIMATION)、提交(CALLBACK_COMMIT),并通过postCallback方法在对应需要同步Vsync刷新处进行注册,等待回调,关于这个细节和原理可以看Android图形系统-Choreographer和Android垂直同步和三重缓存,这里我们并不深究Choreographer机制和Vsync机制,我们看到注释1中的Choreographer的postCallback方法提交了CALLBACK_TRAVERSAL类型的回调,它对应着mTraversalRunnable绘制操作,而mTraversalRunnable是一个TraversalRunnable类型的绘制任务,最终回调会执行这个任务,mTraversalRunnable的run方法源码如下:
1 | //ViewRootImpl.java |
doTraversal()里面会执行performTraversals方法,点开doTraversal方法看一下,如下:
1 | //ViewRootImpl.java |
在doTraversal() 方法里面我们终于看到我们熟悉的方法:performTraversals()。
View树绘制的起点 - performTraversals()
performTraversals()它是整个View Hierarchy绘制的起点,它里面会执行View绘制的三大工作流程,我们先看一下精简版的performTraversals方法,如下:
1 | //ViewRootImpl.java |
performTraversals方法里面非常复杂,我们看的时候千万不要深究其中的细节,不然就走火入魔了,我们找出个整体框架就行,我们先看注释1、2、3,可以看到依此调用measureHierarchy() -> performLayout() -> performDraw(),而measureHierarchy()里面最终调用performMeasure(),所以performTraversals()可以看作依此调用了performMeasure() -> performLayout() -> performDraw(),分别对应顶级View的measure、layout和draw流程,顶级View可以理解为View Hierarchy的根节点,它一般是一个ViewGroup,就像Activity的DecorView一样。
ps:
1、在performTraversals()方法中,performMeasure()可能会执行多次,而performLayout()和performDraw()最多执行一次。
2、本文讨论的顶级View你可以把它类比成Activity的DecorView,但是它其实就是View树的根结点,DecorView也是Activity中View树的根结点。
接下来我们就照着performTraversals() 中的整体框架来讲解View工作的三大流程。
View的测量流程 - performMeasure()
1、MeasureSpec
讲解View的measure流程前,不得不先讲解一下MeasureSpec的含义,MeasureSpec是一个32位的int值,它是View的一个内部类,它的高2位代表着SpecMode,表示测量模式,它的低30位表示SpecSize,表示测量大小,系统通过位运算把SpecMode和SpecSize合二为一组成一个32位int值的MeasureSpec。
下面看一下MeasureSpec的里面组成,如下:
1 | //View.java |
可以看到MeasureSpec提供了三个工具方法分别用来组合MeasureSpec、从MeasureSpec中取出SpecMode、从MeasureSpec中取出SpecSize,其中SpecMode有三种取值,如下:
- UNSPECIFIED:它表示父容器对子View的绘制的大小没有任何限制,要多大给多大,这种情况一般适用于系统内部,表示一种测量状态。
- EXACTLY:它表示父容器已经测量出子View需要的精确大小SpecSize,这个时候View的最终大小就是SpecSize的值,它对应于LayoutParams中match_parcent和具体的数值这两种模式。
- AT_MOST:它表示父容器为子View的大小指定了一个最大值SpecSize,这个时候View的大小不能大于这个值,它对应于LayoutParams中的wrap_content这种模式。
1.1 如何确定View的MeasureSpec?
除了顶级View,其他View的MeasureSpec都是由父容器的MeasureSpec和自身的LayoutParams共同决定的,LayoutParams就是你平时在编写View的xml属性时那些带有layout_XX前缀开头的布局属性,对于顶级View和在View树中子View的MeasureSpec的生成规则有点不一样,见下面分析:
1.1.1、顶级View的MeasureSpec的创建 - getRootMeasureSpec()
由于顶级View是View树的根结点,所以它没有父容器,所以它的MeasureSpec是由屏幕窗口的尺寸和自身的LayoutParams来共同决定,上面注释1.1我们讲到顶级View在调用performMeasure方法之前,会先调用ViewRootImpl的getRootMeasureSpec方法来生成自身宽和高的MeasureSpec,我们来看一下getRootMeasureSpec方法,如下:
1 | //ViewRootImpl.java |
windowSize就是是传入的desiredWindowWidth或desiredWindowHeight,它表示屏幕的大小,rootDimension就是传入的屏幕窗口的LayoutParams的大小模式,对应我们平时写的layout_width或layout_height属性,该属性无非就三个值:match_parent、wrap_content和固定的数值,所以从getRootMeasureSpec方法可以看到,顶级View的MeasureSpec的创建规则如下:
其中rootSize表示顶级View大小。
1.1.2、子View的MeasureSpec的创建 - getChildMeasureSpec()
在1中,顶级View的MeasureSpec已经创建好了,这时候就要根据这个MeasureSpec去生成子View的MeasureSpec,子View的MeasureSpec的创建是从ViewGroup的measureChildWithMargins方法开始,如下:
1 | //ViewGroup.java |
上述方法会对子View进行measure,由注释1得知,在调用子View的measure方法前,会先调用getChildMeasureSpec方法获得子View的MeasureSpec,从getChildMeasureSpec方法的参数可以看出,子View的MeasureSpec的创建与父容器的MeasureSpec和子View本身的LayoutParams有关,此外还和View的margin及padding有关,下面我们来看ViewGroup的getChildMeasureSpec方法,如下:
1 | //ViewGroup.java |
可以看到getChildMeasureSpec方法里面的逻辑还是很清楚的,首先根据父容器的测量模式specMode分为三大类:EXACTLY、AT_MOST和UNSPECIFIED,每一类又和子View的LayoutParams的的三种大小模式:固定大小、MATCH_PARENT和WRAP_CONTENT组合,所以总共有3 X 3 = 9种组合,所以根据getChildMeasureSpec方法可以得出子View的MeasureSpec的创建规则如下:
其中childSize表示子View的大小,parentSize表示父容器剩余大小。
2、View和ViewGroup的measure流程
分析完View的MeasureSpec的创建后,我们继续回到View的measure流程,大家都知道ViewGroup是继承自View的,所以View的measure流程,分为两种情况,一种是View的measure流程,一种是ViewGroup的measure流程,但是不管是View的measure流程还是ViewGroup的measure流程都是从ViewRootImpl的performMeasure()开始,并且都会先调用View的measure方法,如下:
1 | //ViewRootImpl.java |
我们继续看View的measure方法,如下:
1 | //View.java |
可以看到measure方法是一个final方法,说明这个方法不能够被子类重写,这个方法把measure的具体过程交给了onMeasure方法去实现,所以View和ViewGroup的measure流程的差异就从这个onMeasure方法开始,见下面分析。
2.1、View的measure流程
从上述知道View的measure起点在View的measure方法中,并且View的measure方法会调用View的onMeasure方法,View::measure() -> View::onMeasure(),所以我们直接看onMeasure方法在View中的实现,如下:
1 | //View.java |
View中的onMeasure方法的默认实现是先调用getDefaultSize方法获取默认宽高,然后再调用调用setMeasuredDimension方法设置View的宽高,当调用setMeasuredDimension方法设置View的宽高后,就可以通过getMeasureWidth()或getMeasureHeight()获得View测量的宽高,所以我们先看一下 getDefaultSize()方法是如何获取默认的宽高,该方法源码如下:
1 | //View.java |
getDefaultSize方法的逻辑很简单,除了UNSPECIFIED这种模式,其他测量模式都返回MeasureSpec中的specSize,而这个specSize就等于父容器给View测量后的大小,所以我们可以得出一个结论:直接继承View写自定义控件时需要重写onMeasure方法并设置wrap_content时自定义View自身的大小,这是因为如果自定义View在xml文件写了layout_XX = wrap_content这个属性,那么在创建它的MeasureSpec时,它的specMode就会等于AT_MOST,而从getDefaultSize方法看出,如果specMode是AT_MOST或EXACTLY,它们两个返回的值是一样的,都是MeasureSpec中的specSize,通过上面所讲的子View的MeasureSpec的创建规则可知specSize是等于parentSize即父容器剩余的大小,这样就会造成这个自定义View会填充满整个父容器,效果和match_parent一样,并不按你想象那样的大小。所以以后在自定义View时,如果有wrap_content这个场景,就要重写onMeasure方法,可以参考下面的模板,如下:
1 | //View.java |
讲完了getDefaultSize()中AT_MOST和EXACTLY模式情况,接着讲UNSPECIFIED这种模式的情况,从getDefaultSize方法中可以看出如果specMode是UNSPECIFIED,返回的大小就是传进来的size,而这个size就是通过getSuggestedMinimumWidth()或getSuggestedMinimumHeight()方法获得的,所以我们以getSuggestedMinimumWidth方法为例子,看一些如果获取在UNSPECIFIED模式下的宽,getSuggestedMinimumHeight()方法同理,getSuggestedMinimumWidth方法源码如下:
1 | //View.java |
mBackground就等于View的背景,即android:background属性,mMinWidth就等于你在View的xml布局中写了“android:minWidth”这个属性,mBackground.getMinimumWidth()就是获取View的背景的宽度,所以我们得出结论:在UNSPECIFIED模式下,如果View没有设置背景,那么View的宽就等于android:minWidth,如果View设置了背景,那么View的宽就等于View的背景background的宽和android:minWidth的最大值,高度同理。
View的onMeasure方法执行完后,就可以通过getMeasureWidth()或getMeasureHeight()获得View测量的宽高,但是有可能会不准确,因为有时候系统会进行多次measure,才能确定最终测量宽高,所以最好是在onLayout方法中去获取View的宽高。
2.2、ViewGroup的measure流程 (以FrameLayout为例)
从上述知道ViewGroup的measure起点也在View的measure方法中,而View的measure方法会调用View的onMeasure方法,ViewGroup继承自View,但是它是一个抽象类并没有重写View的onMeasure方法,而是由ViewGroup的子类如LinearLayout、FrameLayout等重写onMeasure方法以实现不同的measure流程,这里以FrameLayout为例,*View::measure() -> FrameLayout::onMeasure() *,我们来看FrameLayout的onMeasure方法,如下:
1 | //FrameLayout.java |
可以看到与View的onMeasure方法不同的是,FrameLayout的onMeasure方法是遍历它所有的子View,然后逐个测量子View的大小,这个测量子View是通过注释1的measureChildWithMargins方法来完成,这个方法已经在上面子View的MeasureSpec的创建中讲过一点,measureChildWithMargins方法是在FrameLayout的父类ViewGroup中,如下:
1 | protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { |
measureChildWithMargins方法中首先会根据父容器传进来的parenXXMeasureSpec来创建子View的childXXMeasureSpec,然后调用子View的measure方法,把测量子View的任务又推给了子View,这个过程又回到了2.1所讲的View的measure流程,就不再赘述,所有子View测量完后,ViewGroup就可以得出自己的测量宽高。
3、小结
measure流程是三大流程中最复杂的一个,它的整体流程是:从ViewRootImp的performTraversals()方法进入performMeasure()方法,开始整颗View树的测量流程,在performMeasure方法里面会调用View的measure方法,然后measure方法会调用onMeasure方法,如果是View就直接开始测量,设置View的宽高,如果是ViewGroup,则在onMeasure方法中则会对所有的子View进行measure过程,如果子View是一个ViewGroup,那么继续向下传递,直到所有的View都已测量完成。如图:
measure过后就可以通过getMeasureWidth()或getMeasureHeight()获得View测量的宽高。
View的布局流程 - performLayout()
前面讲解了View的measure过程,如果你理解了,那么View的布局过程也很容易理解的,和measure相似,View的布局过程是从ViewRootImpl的performLayout()开始的,如下:
1 | //ViewRootImpl.java |
在performLayout中主要调用了顶级View的layout方法,顶级View的实例有可能是View也有可能是ViewGroup,但是这个layout方法是在View中,它不像measure方法那样,它不是final修饰,所以它可以被重写,并且ViewGroup重写了layout方法,我们先看一下ViewGroup中的layout方法,如下:
1 | //ViewGroup.java |
可以看到ViewGroup重写的layout方法只是做了一些判断,然后最终还是还是调用了父类即View的layout方法,所以我们直接看View的layout方法即可。
1、View和ViewGroup的layout流程
View的layout方法如下:
1 | //View.java |
layout方法传进来的l、t、r、b分别代表着View的上下左右四个点的坐标,这个四个点的坐标是相对于它的父容器来说的,这个layout方法主要干了两件事:
- 1、注释1:调用View的setFrame方法设定View的四个顶点的位置,我们先看View的setFrame()方法,如下:
1 | //View.java |
可以看到,setFrame方法主要把l、t、r、b分别赋值给mLeft、mTop、mBottom、mRight,即更新View的四个顶点的位置,这个四个顶点一旦确定,那么View在父容器中的位置也就确定了。
- 2、我们继续看注释2:调用了onLayout方法,这个方法在View中是一个空实现,如下:
1 | //View.java |
但是在ViewGroup中是一个抽象方法,如下:
1 | //ViewGroup.java |
这是因为onLayout方法主要用途是给父容器确定子View的位置,所以如果本身就是一个View,就无需实现这个方法,但是如果是ViewGroup,它还要布局子View,所以是ViewGroup的子类就要强制实现这个方法,不同的ViewGroup具有不同的布局方式,所以不同的ViewGroup的onLayout方法的实现就不一样,我们还是以FrameLayout为例,看一下FrameLayout的onLayout方法的实现,如下:
1 | //FrameLayout.java |
FrameLayout的onLayout方法只调用了layoutChildren方法,该方法如下:
1 | void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) { |
可以发现layoutChildren里面过程和onMeasure里面的过程很像,只是注释1中调用的是子View的layout方法而不是measure方法,如果这个子View是一个View,那么layout方法里面就可以通过setFrame方法直接确定自身的位置,如果这个子View是一个ViewGroup,除了调用setFrame方法确定自身的位置外,还要重复onLayout方法中确定子View位置的过程,最后一层一层的往下,直到全部都子View的layout完成。
2、小结
我们再来看一下layout的整体流程:从ViewRootImp的performTraversals()方法进入performLayout()方法,开始整颗View树的布局流程,在performLayout方法里面会调用layout方法,我们发现,View的布局过程其实也可想测量过程那样分为View的layout流程和ViewGroup的layout流程,对于View来说,执行layout方法时只需要直接确定自身四个顶点的位置即可,而onLayout方法是一个空实现;对于ViewGroup来说,执行layout方法时除了要确定自身的四个顶点的位置外,那么它在onLayout方法中还要对自己所有的子View进行layout,最后一层一层的往下,直到全部都layout完成。如下:
layout过后就可以通过View的getWidth()和getHeight()来获取最终的宽高的,这个两个方法的实现如下:
1 | //View.java |
可以发现就是通过View的四个顶点的差值来得到View的准确宽高。
View的绘制流程 - performDraw()
和上面两步相似,View的绘制从ViewRootImpl的performDraw()开始的,如下:
1 | //ViewRootImpl.java |
performDraw()方法中并不是先调用View的draw方法,而是先调用ViewRootImpl的draw方法,如下:
1 | //ViewRootImpl.java |
在ViewRootImpl的draw方法中首先获取需要绘制的区域,然后判断是否使用GPU进行绘制,使用硬件加速是为提高了Android系统显示和刷新的速度,是在在API 11之后引入GPU加速的支持,关于这部分知识可参考理解Android硬件加速的小白文,不是本文重点,这里我们只关心注释1使用CPU绘制的情况,也就是调用ViewRootImpl的drawSoftware方法来绘制,ViewRootImpl的drawSoftware()方法如下:
1 | //ViewRootImpl.java |
drawSoftware方法中主要做了3件事:
- 1、获取Surface对象并锁住Canvas绘图对象
- 2、从View树的根视图开始绘制整颗视图树
- 3、释放Surface对象并解锁Canvas,通知SurfaceFlinger更新视图
1、View和ViewGroup的draw流程
第1和第3点都是操作Surface的基本流程,我们主要看第二点即注释2,调用了View的draw方法,它就是一个模板方法,定义了几个固定的绘制步骤,如下:
1 | //View.java |
你看那英文注释,它已经替我们把draw方法中的6大步骤写出来了,其中最重要的就是注释3和4,我们分别来介绍一下:
- onDraw(canvas):onDraw方法是用来绘制自身内容,如果你的自定义View或ViewGroup需要绘制内容,就要重写这个方法在Canvas上绘制自身内容。
- dispatchDraw(canvas):如果是ViewGroup,除了绘制自身内容外,还需要绘制子View的内容,所以dispatchDraw就是把View的绘制一层一层的传递下去,直到整颗View树绘制完毕,ViewGroup重写了该方法,我们看一下它的主要源码如下:
1 | //ViewGroup.java |
可以看到,dispatchDraw方法把绘制子View的任务通过drawChild方法分发给它的子View,如果是一个ViewGroup,又会重复dispatchDraw()过程。
2、onDraw()绘制开关 - setWillNotDraw()
1 | //View.java |
但是如果你不需要绘制任何内容,你可以通过View的setWillNotDraw(true)方法关闭绘制,在默认情况下,View没有启用这个优化标志位,但是ViewGroup会启用,所以当你的自定义ViewGroup需要通过onDraw来绘制内容时,需要显式的打开这个开关setWillNotDraw(false),当你的自定义View不需要onDraw来绘制内容时,需要显式的关闭这个开关setWillNotDraw(true)。
3、小结
到这里,我们走完了View的绘制过程,我们再来看一下draw的整体流程:从ViewRootImp的performTraversals()方法进入performDraw()方法,开始整颗View树的绘制流程,在performDraw()方法中经过层层调用:ViewRootImpl :: draw() -> ViewRootImpl :: drawSoftware() -> View :: draw(),来到View的draw()方法,它里面定义了View绘制的6大步骤,其中对于View来说,直接调用onDraw()方法绘制自身,对于ViewGroup来说,还要通过dispatchDraw()把绘制子View的流程分发下去,一层层传递,直到所有View都绘制完毕。如图:
总结
我们一直讲View的工作原理,但有没有发现ViewRootImpl也出现的很频繁,它虽然不是一个View,但它是连接View和Window之间的纽带,View三大工作流程的起点就是ViewRootImpl的performTraversals()方法,performTraversals()中依此调用了performMeasure() -> performLayout() -> performDraw(),分别对应顶级View的measure、layout和draw流程,然后顶级View的measure流程和layout流程又会分别调用我们熟悉的onMeasure()、onLayout()方法 ,draw流程有点特别,它是通过dispatchDraw()方法来进行draw流程的传递, 而onDraw()方法只是单纯的绘制自身内容,在onMeasure()方法中会对所有child进行measure过程,同理onLayout()方法中会对所有child进行layout过程,dispatchDraw()方法中会对所有child进行draw过程,如此递归直到完成整颗View Hierarchy的遍历。
该过程如图:
在阅读Android源码时,如果你只是在追踪方法的调用链,这种过程是毫无意义的,但是如果你在这个阅读过程加入了自己的思考,把它的知识点用自己的语言整理,这样才会有所收获。以上就是我对View的工作原理的理解,希望大家有所收获。
参考资料:
《Android开发艺术探索》