Android View的绘制流程分为三大流程:测量、布局、绘制。三大流程都开始于ViewRootImpl
的performTraversals
函数。通过了解三大流程的顺序和原理,支撑日常开发工作。
一、测量流程
三大流程都是始于ViewRootImpl
的performTravels
函数,先是从调用View的performMeasure
函数开始测量流程,再是调用performLayout
函数开始布局流程,进而是调用performDraw
函数开始绘制流程。本节从performMeasure
函数开始,讲View的测量流程。
正式开始测量流程了~
performMeasure
函数会调用View的measure
函数。
measure
函数第一行会调用isLayoutModeOptical
函数,用来判断当前View是否ViewGroup
,是ViewGroup
的话,判断layoutModel
属性是否LAYOUT_MODE_OPTICAL_BOUNDS
,即opticalBounds
。该属性默认为clipBounds
,还可取值opticalBounds
,前者在获取ViewGroup
的四边(getLeft
,getTop
,getRight
,getBottom
)将返回原始的值,而opticalBounds
表示给ViewGroup
加一些特殊的效果,例如阴影或高亮效果,因为返回的四边也将比clipBounds
小。
measure
函数接下来的这一段主要是为了判断是否需要进行重新测量,毕竟每次测量也不容易。
1 | //用于存储上次测量的结果 |
needsLayout
就是根据上面相关变量的值共同判断是否需要重新测量的最终结果。也可以通过下图一览上面的注释。
接着measure
函数的内容,当调用forceLayout
或requestLayout
函数,mPrivalteFlags
就会添加PFLAG_FORCE_LAYOUT
标记,那么forceLayout
就是true
,无论后面其他判断条件怎么样,一定会调用onMeasure
函数进行测量。而needsLayout
就在上文刚分析了。
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
语句重置所有的已设置的测量信息,毕竟要准备重新开始测量了。resolveRtlPropertiesIfNeeded()
主要是处理文本从右到左的情况,因为并不是所有国家文字书写顺序都是从左到右。
LongSparseLongArray
是key
与value
都为Long
,类似HashMap
的数据结构。这里正是通过这种结构用来存储测量的宽和高,如果mMeasureCache.indexOfKey(key)
返回值小于0,表示不存在对应的宽高,需要测量。
sIgnoreMeasureCache
表示为了性能优化而忽略测量缓存,其实是为了兼容旧版本,因为在Android4.4
前,APP总是希望onMeasure
函数被调用,所以该变量总是true
,而Android 4.4
和后续版本,该标志总是false
。
因此,如果需要测量,则调用当前View的onMeasure
函数;不需要重新测量,则从缓存mMeasureCache
获取已缓存宽高。
measure
函数的最后代码就是保存父View对当前View的宽高要求和往mMeasureCache
存值,以供下次测量作为判断条件使用。
measure函数总结一下:
measure函数主要是为了性能优化,根据缓存(已缓存)、父类约束是不与上次一致,和行为(刷新布局)来判断是否重新测量大小。
接下来看看View
的onMeasure
函数做了什么事:
看着简单,其实还是要拆解看看:
getSuggestedZMininumWidth
函数主要判断当前是否设置背景,如果没有设置背景,则取最小宽度;设置了背景,则取最小宽度和背景最小宽度的两者之间的最大值。最小宽度就是我们设置的minWidth
属性。高度的测量亦是如此。
getDefaultSize
函数主要是根据测量模式,计算出默认的尺寸大小。
到这里,就应该需要对MeasureSpec
的大小和测量模式解释一下,不然有的同学真一脸懵逼。MeasureSpec
是View
的静态内部类,代表一个32位的整型,高2位表示测量模式,低30位表示尺寸大小。measure
函数的两个参数widthMeasureSpec
,heightMeasureSpec
,分别代表着父View对子View的宽高约束。从这里也可以看出,子View的大小由父View约束和子View自身自身约束共同确定。
通过MeasureSpec
提供的一些静态方法,如int getSize(int measureSpec)
、int getMode(int measureSpec)
,可以获取到测量模式mode
和大小size
,分别为:
- EXACTLY:当
View
的layout_width
或者layout_height
设置为match_parent
或具体的值时,该测量模式就是EXACTLY
,表示父View
对当前View
的尺寸要求大小是size; - AT_MOST:当
View
的layout_width
或者layout_height
属性设置为wrap_content
,该测量模式就是AT_MOST
,表示父View
能给予当前View
的最大的可用尺寸是size
,具体用多少当前View
自己决定; - UNSPECIFIED:表示父View对当前View没有任何约束,想要多大的尺寸当前View自己决定。
从getDefaultSize
函数对测量模式AT_MOST
和EXACTLY
的处理方式看,自定义View继承View时,要格外注意layout_width
或layout_height
属性值为wrap_content
的情况,因为它的表现就跟match_parent
是一样的,有时需要根据具体情况去更改这种行为。
setMeasureDimension
函数开始跟measure
函数类似,先判读一下layoutModel
是否optical bound
,进行宽高的调整,并调用setMeasureDimensionRaw
函数。
setMeasureDimension
则是简单的赋值,设置mPrivateFlags
标志位。这样就可以通过getMeasuredWidth
与getMeasuredHeight
函数来获取测量的宽高了。注意: 重写onMeasure
函数需要调用setMeasureDimension
函数进行数据缓存。
测量流程也就到此结束了。但仔细一想,发现不对劲,这里测量指的是View
,那么ViewGroup
呢?
ViewGroup
是View
的子类,而View
的measure
函数被被声明成了final
,所以ViewGroup
测量自身或者测量子View
只能重写onMeasure
函数。但在ViewGroup
类仔细寻找,却没有发现重写onMeasure
函数的痕迹。因为具体的ViewGroup
,如LinearLayout
和RelativeLayout
它们各自的测量方式是不一样的,onMeasure
需要它们具体去实现。但ViewGroup
类提供了一些便捷的api
,如measureChildren
、measureChildWithMargins
、measureChild
等等。
翻翻LinearLayout
的onMeasure
函数,最终也会调用View的measure
函数,走View
的测量流程。
因此自定义View
或者ViewGroup
,需要根据自身实现的功能去重写omMeasure
函数,来测量自身或子View的大小
二、布局流程
上一节分析了测量流程,得知了每个View的宽高大小,这一节紧跟着分析布局流程,判断子View
如何在父View
进行定位。performLayout
函数同样是在ViewRootImpl
类的performTraversals
函数中,performMeasure
函数之后。
可以看到,performLayout
函数很快就调用了View
的layout
函数进行布局流程。这里先不跟进去,只需要知道已经进行了一次布局,然后看performLayout
函数的后续内容。
mLayoutRequesters
是一个保存了在布局过程中所有请求布局的View的列表。当列表不为空时候,需要对这些View进行处理。
在布局的过程中,可能View请求布局(即设置了PFLAG_FORCE_LAYOUT
),将它们存到列表mLayoutRequesters
中,然后在布局结束后,第一次通过getValidLayoutRequesters
函数判断这些View
是否需要重新布局,判断条件就是当前View是否可见和设置了PFLAG_FORCE_LAYOUT
标志。
如果返回值validLayoutRequesters
不为空,重新设置他们的标志位PFLAG_FORCE_LAYOUT
,并调用measureHierarchy
函数,对它们进行View层级的测量,测量流程和整个界面测量流程是一致。然后再跟着重新布局一次host.layout()
。
进行第二次判断是否还在布局过程中,有View请求布局,如果有的话,判断有效的需要重新布局的View,这次判断忽略了PFLAG_FORCE_LAYOUT
标志位,除了不可见的View,其他都列为需要有效的。然后留到下次帧再重新来过。
总结一下
在第一次布局的过程中,如果有View需要requestLayout
函数(一般发生在ListView
等的子View),则需要判断这些View是否可见或已经处理了requestLaout
。如果有可见的、未处理requestLayout
的View则需要进行View层次级别的测量,然后重新布局一次。然后进行第二次判断是否有View需要requestlayout
,这次只判断是否可见。如果还有,这些View就留到下一帧进行吧,老子不管了。
再回到第一次布局host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
。
setOpticalFrame
函数最终也会调用setFrame
,只是追加了点效果边距长度。setFrame
函数主要是对当前View在父View的位置进行确定,如果此时定位位置有变(四边有不一致),则changed返回的是true。在setFrame
函数会调用sizeChanged
函数,而sizeChanged
函数会调用onSizeChanged
函数。
onLayout
函数在View中是一个空实现,而在ViewGroup
未重写该方法。因为子View在父View位置,在不同的ViewGroup
表现也是不同的,所以需要具体的ViewGroup
根据自己的特性去重写。但这里我们注意到一个时机,onSizeChanged
函数的回调在onMeasure
函数之后,onLayout
函数之前,在尺寸大小发生变化时会回调该方法。
在调用onLaout
函数后的主要进行OnLayoutChangeListener
的回调和焦点的处理。isLayoutValid
函数表示至少已经经历过一次布局了或者不会再进行其他布局了,就返回true。
到这里,布局流程基本也就结束了。
本节小结
布局流程始于ViewRootImpl
的performTraversals
函数,然后调用自身的performLayout
函数,对View进行布局,布局结束后对布局过程有请求布局的View进行View层级测量和布局。在View的layout
函数中,通过setFrame
对自身进行布局定位,如果位置发生变化则回调onSizeChanged
函数。再而是调用onLayout
函数。因此自定View无需重写onLayout
函数,自定义ViewGroup
则需要重写onLayout
函数进行子View的布局。
三、绘制流程
经过测量、绘制,已经知道了View的大小,在父View的位置,那么接下来就是如何将View绘制出来,展现在屏幕。
绘制流程始于ViewRootImpl
的performTraversals
函数,调用自身的performDraw
函数。
1 | //ViewRootImpl.java |
在draw
函数中,主要是绘制区域dirty
的确定,例如是否滚动、全部绘制等。
drawSoftware
函数就是通过软件去绘制的地方,主要根据dirty
区域,生成并锁定canvas
,而canvas
就是绘制内容的区域。
而在View的draw
函数,则是View的绘制的开始:
1 | drawBackground=>onDraw=>dispatchDraw=>onDrawForeground=>drawDefaultFocusHighlight |
在View的draw
流程中,自定义View
一般重写onDraw
函数,super.onDraw
后绘制自己的内容,表示所绘制内容在系统绘制的内容之后。而自定义ViewGroup
中,如果需要覆盖在子View之上,应该是重写dispatchDraw
函数,并调用super.dispatchDraw
之后,因为dispatchDraw
函数会去绘制所有子View的内容,在之前绘制的内容都会被覆盖。当然,也可以以dispatchDraw
作为分界点,根据需要重写其他函数,绘制内容。
如果重写ViewGroup
的onDraw
函数,绘制的内容一般显示不出来,因为ViewGroup
会优化从而跳过onDraw
函数,可以通过设置背景或setWillNotDraw(false)
来解决这个问题。
四、总结
View的工作流程出发于ViewRootImpl
类,并在其他performTraversals
函数开始View的测量、布局和绘制。
在测量阶段,子View的宽高受自身和父View的共同约束,而父View一般都是xml布局中layout_
开头的属性,而DecorView
则受自身和手机屏幕尺寸约束。其中涉及到一个比较重要的概念是MeasureSpec
的使用。布局阶段主要定位子View在父View的位置,进行排版。绘制阶段主要对View的内容进行绘制,要注意View
和ViewGroup
几个方法的绘制顺序。
通过学习Android
的绘制流程,需要知道几点情况:
- 自定View时,需要考虑宽高设置
wrap_content
的情况,因为它的表现在测量阶段和match_parent
是一致的。 - 重写View的
onDraw
函数,要避免在onDraw
创建对象,因为onDraw
会被调用多次,可以考虑在onSizeChanged
函数创建。 - 如果
View
或ViewGroup
需要改变自身大小,应该在onMeasure
函数实现,并通过setMeasureDimension
保存下来。 - 重写
ViewGroup
的onDraw
函数时,要注意onDraw
函数在整个draw流程的地位,以及它并不是都会被调用。
番外篇
1、MeasureSpec
是什么?作用
MeasureSpec
在View中的一个静态内部类,能将一个32位整型拆分成测量模式和测量大小,代表着父View对子View的约束。32位的整型,高两位代表着测量模式,低三十位代表测量大小。通过位位运算,可以分别获取测量模式和大小,而合并成一个32位整型,只需要相加即可。
例如,给宽设置10,此时测量模式是精确模式EXACTLY,即01,用32位的整型表示应该是(暂且用xxx表示中间所有的0)
若求测试模式model,只需要和高两位都是1,低三十位都是0的MODE_MASK
按位与即可。
代码:
1 | public static int getMode(int measureSpec) { |
若求测试大小,只需要和取反后MODE_MASK
按位与即可。
代码:
1 | public static int getSize(int measureSpec) { |
2、requestLayout
和invalidate
,postInvalidate
区别?
一般来说,需要重新走整个流程,就调用requestLayout
,然后再调用invalidate
保证onDraw
一定被调用。也就是说requestLayout
不一定保证onDraw
被调用,但会调用onMeasure
函数和onLayout
函数。而invalidata
函数只会调用到onDraw
函数。invalidate
函数在UI线程刷新界面,postInvalidate
表示在子线程刷新界面。