Android进阶:绘制流程

3,868 阅读4分钟

Android View的绘制流程分为三大流程:测量、布局、绘制。三大流程都开始于ViewRootImpl的performTraversals函数。通过了解三大流程的顺序和原理,支撑日常开发工作。绘制是Android进阶拦路虎之一。

一、测量流程

三大流程都是始于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函数接下来的这一段主要是为了判断是否需要进行重新测量,毕竟每次测量也不容易。

	//用于存储上次测量的结果
        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()主要是处理文本从右到左的情况,因为并不是所有国家文字书写顺序都是从左到右。 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_MOSTEXACTLY的处理方式看,自定义View继承View时,要格外注意layout_widthlayout_height属性值为wrap_content的情况,因为它的表现就跟match_parent是一样的,有时需要根据具体情况去更改这种行为。

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

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

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

ViewGroup是View的子类,而View的measure函数被被声明成了final,所以ViewGroup测量自身或者测量子View只能重写onMeasure函数。但在ViewGroup类仔细寻找,却没有发现重写onMeasure函数的痕迹。具体原因是因为具体的ViewGroup,如LinearLayout和RelativeLayout它们各自的测量方式是不一样的,onMeasure需要它们具体去实现。但ViewGroup类提供了一些便捷的api,如measureChildrenmeasureChildWithMarginsmeasureChild等等。 翻翻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函数。

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

draw函数中,主要是绘制区域dirty的确定,例如是否滚动、全部绘制等。

drawSoftware函数就是通过软件去绘制的地方,主要根据dirty区域,生成并锁定canvas,而canvas就是绘制内容的区域。

而在View的draw函数,则是View的绘制的开始:

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)来解决这个问题。

四、总结

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

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

番外篇

1、MeasureSpec是什么?作用

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

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

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

代码:

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

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

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

2、requestLayout和invalidate,postInvalidate区别?

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