Android 绘制流程

Android View的绘制流程分为三大流程:测量、布局、绘制。三大流程都开始于ViewRootImplperformTraversals函数。通过了解三大流程的顺序和原理,支撑日常开发工作。

一、测量流程

三大流程都是始于ViewRootImplperformTravels函数,先是从调用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//用于存储上次测量的结果
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

//view是否需要强行刷新,调用froceLayout
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

//判断此次的widthMeasureSpec与heightMeasureSpec是否与上次相等
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;

//判断此次测量模式是否精确,不是精确的可能需要重新测量
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;

//判断此次测量大小是否与已保存的大小一致,不是一致可能需要重新测量
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);

//如果specChanged为false,即宽高measureSpec与上次都相等,不需要重新测量;true则进一步检查其他条件
//sAlwaysRemeasureExactly主要用于判断LinearLayout在旧版本的不同测量模式都会返回不同的测量结果,小于Android 6.0为true,大于为false;所以但小于Android 6.0需要重新测量
//如果isSpecExactly测量模式是非精确模式需要重新测量
//如果matchesSpecSize与已保存大小不一致需要重新测量
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

needsLayout就是根据上面相关变量的值共同判断是否需要重新测量的最终结果。也可以通过下图一览上面的注释。

接着measure函数的内容,当调用forceLayoutrequestLayout函数,mPrivalteFlags就会添加PFLAG_FORCE_LAYOUT标记,那么forceLayout就是true,无论后面其他判断条件怎么样,一定会调用onMeasure函数进行测量。而needsLayout就在上文刚分析了。

mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;语句重置所有的已设置的测量信息,毕竟要准备重新开始测量了。resolveRtlPropertiesIfNeeded()主要是处理文本从右到左的情况,因为并不是所有国家文字书写顺序都是从左到右。

LongSparseLongArraykeyvalue都为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函数主要是为了性能优化,根据缓存(已缓存)、父类约束是不与上次一致,和行为(刷新布局)来判断是否重新测量大小。

接下来看看ViewonMeasure函数做了什么事:

看着简单,其实还是要拆解看看:

getSuggestedZMininumWidth函数主要判断当前是否设置背景,如果没有设置背景,则取最小宽度;设置了背景,则取最小宽度和背景最小宽度的两者之间的最大值。最小宽度就是我们设置的minWidth属性。高度的测量亦是如此。

getDefaultSize函数主要是根据测量模式,计算出默认的尺寸大小。

到这里,就应该需要对MeasureSpec的大小和测量模式解释一下,不然有的同学真一脸懵逼。MeasureSpecView的静态内部类,代表一个32位的整型,高2位表示测量模式,低30位表示尺寸大小。measure函数的两个参数widthMeasureSpecheightMeasureSpec,分别代表着父View对子View的宽高约束。从这里也可以看出,子View的大小由父View约束和子View自身自身约束共同确定。

通过MeasureSpec提供的一些静态方法,如int getSize(int measureSpec)int getMode(int measureSpec),可以获取到测量模式mode和大小size,分别为:

  • EXACTLY:当Viewlayout_width或者layout_height设置为match_parent或具体的值时,该测量模式就是EXACTLY,表示父View对当前View的尺寸要求大小是size;
  • AT_MOST:当Viewlayout_width或者layout_height属性设置为wrap_content,该测量模式就是AT_MOST,表示父View能给予当前View的最大的可用尺寸是size,具体用多少当前View自己决定;
  • UNSPECIFIED:表示父View对当前View没有任何约束,想要多大的尺寸当前View自己决定。

getDefaultSize函数对测量模式AT_MOSTEXACTLY的处理方式看,自定义View继承View时,要格外注意layout_widthlayout_height属性值为wrap_content的情况,因为它的表现就跟match_parent是一样的,有时需要根据具体情况去更改这种行为。

setMeasureDimension函数开始跟measure函数类似,先判读一下layoutModel是否optical bound,进行宽高的调整,并调用setMeasureDimensionRaw函数。

setMeasureDimension则是简单的赋值,设置mPrivateFlags标志位。这样就可以通过getMeasuredWidthgetMeasuredHeight函数来获取测量的宽高了。注意: 重写onMeasure函数需要调用setMeasureDimension函数进行数据缓存。

测量流程也就到此结束了。但仔细一想,发现不对劲,这里测量指的是View,那么ViewGroup呢?

ViewGroupView的子类,而Viewmeasure函数被被声明成了final,所以ViewGroup测量自身或者测量子View只能重写onMeasure函数。但在ViewGroup类仔细寻找,却没有发现重写onMeasure函数的痕迹。因为具体的ViewGroup,如LinearLayoutRelativeLayout它们各自的测量方式是不一样的,onMeasure需要它们具体去实现。但ViewGroup类提供了一些便捷的api,如measureChildrenmeasureChildWithMarginsmeasureChild等等。
翻翻LinearLayoutonMeasure函数,最终也会调用View的measure函数,走View的测量流程。

因此自定义View或者ViewGroup,需要根据自身实现的功能去重写omMeasure函数,来测量自身或子View的大小

二、布局流程

上一节分析了测量流程,得知了每个View的宽高大小,这一节紧跟着分析布局流程,判断子View如何在父View进行定位。performLayout函数同样是在ViewRootImpl类的performTraversals函数中,performMeasure函数之后。

可以看到,performLayout函数很快就调用了Viewlayout函数进行布局流程。这里先不跟进去,只需要知道已经进行了一次布局,然后看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。


到这里,布局流程基本也就结束了。

本节小结

布局流程始于ViewRootImplperformTraversals函数,然后调用自身的performLayout函数,对View进行布局,布局结束后对布局过程有请求布局的View进行View层级测量和布局。在View的layout函数中,通过setFrame对自身进行布局定位,如果位置发生变化则回调onSizeChanged函数。再而是调用onLayout函数。因此自定View无需重写onLayout函数,自定义ViewGroup则需要重写onLayout函数进行子View的布局。

三、绘制流程

经过测量、绘制,已经知道了View的大小,在父View的位置,那么接下来就是如何将View绘制出来,展现在屏幕。

绘制流程始于ViewRootImplperformTraversals函数,调用自身的performDraw函数。

1
2
//ViewRootImpl.java
performTraversals=>performDraw=>draw=>drawSoftware=>View.draw

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作为分界点,根据需要重写其他函数,绘制内容。

如果重写ViewGrouponDraw函数,绘制的内容一般显示不出来,因为ViewGroup会优化从而跳过onDraw函数,可以通过设置背景或setWillNotDraw(false)来解决这个问题。

四、总结

View的工作流程出发于ViewRootImpl类,并在其他performTraversals函数开始View的测量、布局和绘制。

在测量阶段,子View的宽高受自身和父View的共同约束,而父View一般都是xml布局中layout_开头的属性,而DecorView则受自身和手机屏幕尺寸约束。其中涉及到一个比较重要的概念是MeasureSpec的使用。布局阶段主要定位子View在父View的位置,进行排版。绘制阶段主要对View的内容进行绘制,要注意ViewViewGroup几个方法的绘制顺序。

通过学习Android的绘制流程,需要知道几点情况:

  1. 自定View时,需要考虑宽高设置wrap_content的情况,因为它的表现在测量阶段和match_parent是一致的。
  2. 重写View的onDraw函数,要避免在onDraw创建对象,因为onDraw会被调用多次,可以考虑在onSizeChanged函数创建。
  3. 如果ViewViewGroup需要改变自身大小,应该在onMeasure函数实现,并通过setMeasureDimension保存下来。
  4. 重写ViewGrouponDraw函数时,要注意onDraw函数在整个draw流程的地位,以及它并不是都会被调用。

番外篇

1、MeasureSpec是什么?作用

MeasureSpec在View中的一个静态内部类,能将一个32位整型拆分成测量模式和测量大小,代表着父View对子View的约束。32位的整型,高两位代表着测量模式,低三十位代表测量大小。通过位位运算,可以分别获取测量模式和大小,而合并成一个32位整型,只需要相加即可。

例如,给宽设置10,此时测量模式是精确模式EXACTLY,即01,用32位的整型表示应该是(暂且用xxx表示中间所有的0)

若求测试模式model,只需要和高两位都是1,低三十位都是0的MODE_MASK按位与即可。

代码:

1
2
3
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}

若求测试大小,只需要和取反后MODE_MASK按位与即可。

代码:

1
2
3
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}

2、requestLayoutinvalidate,postInvalidate区别?

一般来说,需要重新走整个流程,就调用requestLayout,然后再调用invalidate保证onDraw一定被调用。也就是说requestLayout不一定保证onDraw被调用,但会调用onMeasure函数和onLayout函数。而invalidata函数只会调用到onDraw函数。invalidate函数在UI线程刷新界面,postInvalidate表示在子线程刷新界面。