掌握 View 绘制流程能对视图的各个绘制时机有更深刻的认识,并且能写出更好的自定义 View, 反正看源码(SDK28)就完了。

一、介绍

二、源码分析

  1. measure
  2. layout
  3. draw

三、总结

一、介绍

Activity 是通过 Window 与 View系统进行交互,而 Window 则是通过 ViewRootImpl 与 根View(DecorView)交互,View 最关键的三个步骤就是测量(measure)、布局(layout)、绘制(draw), 最开始绘制的入口是 ViewRootImpl 类的 performTravesals 方法,下图对整体流程做了个概述:

二、源码分析

1. measure

MeasureSpec: 这个关键对象贯穿在测量流程中,我们可以把它理解成一个 View 自身的「测量规格」, 它包含两个变量一个是 mode(测量模式),另一个是 size(测量尺寸)。

我觉得源码有一点设计的特别巧妙,但也很难理解,那就是用位操作来表示某个状态值。这么做的原因是能节省更多的内存以及计算更快。MeasureSpec 是一个数据结构,但是它主要是用来制作一个 int 整型的变量,这个变量高 2 位表示测量模式,低 30 位表示测量尺寸,这是根据模式的数量决定的,总共就三种模式,因此用两位就很够了,如 01000000000000000000001111010101 粗体即表示模式。两个变量合并成一个变量了,看到这种方式简直就像发现新大陆一般。。但不推荐自己写代码的时候用这种方式,因为别人不一定看得懂,可读性差。。

三种模式:

  • UNSPECIFIED: 父视图不强加任何约束给子视图,子视图想多大就多大,此模式一般不会用到,以下讨论就略过这个模式了。
  • EXACTLY: 精确模式,父视图已经知道子视图确切的尺寸,一般对应 match_parent 和 具体数值。
  • AT_MOST: 最大模式,在父视图允许的范围内,子视图尽量的大,一般对应 wrap_content。

LayoutParams: 布局参数。每个 View 都有自身的布局参数,最最基础的就是宽高,我们平时最常见的就是设置width 和 height 为 match_parent 或 wrap_content。然后不同的 LayoutParams 有不同的属性,如 LinearLayout.LayoutParams 就增加了 margin 相关的属性。

View 自身的 MeasureSpec 是由父视图的 MeasureSpec 和 自身的 LayoutParams 一起决定的,接着 View 根据自身的 MeasureSpec 来确定自身测量后的宽/高。

从入口 ViewRootImpl.java 的 performTraversals 方法开始看,它调用 performMeasure 之前做了如下操作:

// ViewRootImpl.java
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
复制代码

mWidth, mHeight 表示屏幕的宽高,lp.width, lp.height 表示 DecorView 的宽高属性,对于 DecorView 来说其 width 和 height 都是 match_parent,因此它的尺寸就是屏幕的尺寸,看下 getRootMeasureSpec 方法做了啥:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {int measureSpec;switch (rootDimension) {case ViewGroup.LayoutParams.MATCH_PARENT:// Window can't resize. Force root view to be windowSize.measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);break;case ViewGroup.LayoutParams.WRAP_CONTENT:// Window can resize. Set max size for root view.measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);break;default:// Window wants to be an exact size. Force root view to be that size.measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);break;}return measureSpec;
}
复制代码

若布局参数中的宽/高是 MATCH_PARENT, 那么它最终得到的「测量规格」的 mode 是 EXACTLY, size 是屏幕宽/高,MeasureSpec.makeMeasureSpec 方法就是合并了 mode 和 size, 制作了一个 measureSpec 变量;若布局参数中的宽或高是 WRAP_CONTENT, 那么它最终得到的「测量规格」的 mode 是 AT_MOST, size 是屏幕宽/高,乍一看其实尺寸和 MATCH_PARENT 是一样的,所以一般系统定义的控件或者我们自定义 View 都会对 WRAP_CONTENT 进行处理,否则其实它的效果在大部分情况下和 MATCH_PARENT 并无一致;若是其他值(一般用户提供了精确的大小),那么它最终得到的「测量规格」的 mode 是 EXACTLY, size 是用户给定的值。

在求出 DecorView 的「测量规格」后,调用 performMeasure 方法,内部主要是调用了 DecorView 的 measure 方法。由于 measure 方法用 final 修饰了,因此子类无法重写此方法,所有的视图都统一经过 View 中的 measure 这个方法。

// View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {// 前半部分代码主要做了优化,若宽高都不变的情况下// 或没有强制重新布局的标志位,那就不重新 measure 了...onMeasure(widthMeasureSpec, heightMeasureSpec);...
}
复制代码

可以把 measure 方法看做是一个统一的测量入口,做了一些通用的事情,真正的测量是在 onMeasure 方法,这个方法是 View 提供给各个子类去实现的,这里大家能自定义很多测量逻辑,如 LinearLayout 布局容器就是通过此方法获取垂直、水平线性布局时自身的宽/高,反正总之就是一句话, measure 流程就是为了求出自身测量后的宽/高,并保存下来。现在看下 View 默认的 onMeasure 实现:

// View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
复制代码

getSuggestedMinimumWidth 方法就是看下是否有背景,如果有就获取背景的宽度,否则看下是否设置了 minWidth 属性,getSuggestedMinimumHeight同理。在这里直接就无视这两个情况吧,正常来说这个方法返回值是 0, 看下 getDefaultSize :

public static int getDefaultSize(int size, int measureSpec) {int result = size;int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);switch (specMode) {case MeasureSpec.UNSPECIFIED:result = size;break;case MeasureSpec.AT_MOST:case MeasureSpec.EXACTLY:result = specSize;break;}return result;
}
复制代码

根据「测量规格」获取测量模式和测量尺寸, 跳过 UNSPECIFIED 模式,当模式为 AT_MOST 和 EXACTLY 时,最原始的 View 视图无论是指定 match_parent 还是 wrap_content 模式,最后的 size 都是「测量规格」的 size, 所以对于不重写 onMeasure 方法的 View 来说,这两个模式没差别。setMeasuredDimension 也是一个 final 修饰的方法,任何视图都统一将宽/高保存成全局变量以便之后使用。以上就是 View 默认的测量流程,下面看下 ViewGroup 自定义实现的 onMeasure 方法。

由于 DecorView 继承自 FrameLayout,因此接下来的流程其实会调用到 FrameLayout 中的 onMeasure, 不过本文不分析 FrameLayout ,而是分析比较常用的 LinearLayout 重写的 onMeasure 方法,我们只分析垂直方向的:

// LinearLayout.java
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {for (int i = 0; i < count; ++i) {final View child = getVirtualChildAt(i);......measureChildBeforeLayout(child, i, widthMeasureSpec, 0,heightMeasureSpec, usedHeight);final int childHeight = child.getMeasuredHeight();......final int totalLength = mTotalLength;mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin              + lp.bottomMargin + getNextLocationOffset(child));......}maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec,                      childState), heightSizeAndState);
}
复制代码

这里不分析 weight 属性,加上这个属性就有点复杂了。首先遍历子视图,让每个子视图都执行自身的 onMeasure 方法,这个过程在 measureChildBeforeLayout 方法内,一会儿在分析。测量子 View 之后,child.getMeasuredHeight() 就能获得这一波测量后的高度了,mTotalLength 可以看做是目前 child 在竖直方向累加的高度(包括padding, margin)。最后调用 setMeasuredDimension 表示这次测量结束,会记录测量后的宽和高。measureChildBeforeLayout 内部会直接调用 measureChildWithMargins, 此方法是父容器测量子视图的统一入口:

// ViewGroup.java
protected void measureChildWithMargins(View child,int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed) {final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();final int   = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin+ widthUsed, lp.width);final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin+ heightUsed, lp.height);child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
复制代码

是否还记得之前说的 View 的「测量规格」是由父视图的「测量规格」和自身的布局参数决定的,这里 childWidthMeasureSpec 就是通过 父视图的「测量规格」+ 自身的布局参数 + padding + margin + 已使用的宽/高 决定的。

// ViewGroup.java
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {// 这是父容器的测量模式int specMode = MeasureSpec.getMode(spec);// 这是父容器的测量尺寸(宽/高)int specSize = MeasureSpec.getSize(spec);int size = Math.max(0, specSize - padding);int resultSize = 0;int resultMode = 0;switch (specMode) {// Parent has imposed an exact size on us// 父容器是精确模式 EXACTLY case MeasureSpec.EXACTLY:// 子视图有一个精确的尺寸,那么它的测量尺寸也就是这个大小,// 并且指定它的模式为 EXACTLYif (childDimension >= 0) {resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;}// 子视图布局的宽/高是 MATCH_PARENT,那么它的大小就是父容器的大小,// 并且指定它的模式为 EXACTLY,这里就能看出,一般精确值和 MATCH_PARENT 对应 EXACTLYelse if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size. So be it.resultSize = size;resultMode = MeasureSpec.EXACTLY;} // 子视图布局的宽度是 WRAP_CONTENT,那么它的大小就是父容器的大小,// 并且指定它的模式为 AT_MOST,所以一般来说自定义View要重写onMeasure。else if (childDimension == LayoutParams.WRAP_CONTENT) {// Child wants to determine its own size. It can't be// bigger than us.resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;// Parent has imposed a maximum size on us// 父容器是最大模式 AT_MOST case MeasureSpec.AT_MOST:// 这里的逻辑和父容器为精确模式时完全一样,// 看起来子视图指定了精确值就不受父容器的约束了if (childDimension >= 0) {// Child wants a specific size... so be itresultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} // 和父容器精确模式相比,大小都是父容器的大小,// 测量模式跟随父容器的模式。else if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size, but our size is not fixed.// Constrain child to not be bigger than us.resultSize = size;resultMode = MeasureSpec.AT_MOST;} // 依然和父容器精确模式一样else if (childDimension == LayoutParams.WRAP_CONTENT) {// Child wants to determine its own size. It can't be// bigger than us.resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;......// 最后制作一个子View自身的「测量规格」return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
复制代码

上面的注释写的比较清晰了,总结下获取子视图 MeasureSpec 的过程:如果子 View 布局参数的尺寸是精确值,那么父容器的 mode 不会影响到子视图,子视图都是 EXACTLY 模式 + 精确值尺寸;如果子 View 的宽/高是 MATCH_PARENT, 那么子视图跟随父容器模式 + 父容器尺寸;如果子 View 的宽/高是 WRAP_CONTENT,那么子视图是 AT_MOST 模式 + 父容器尺寸。

在获得子视图的「测量规格」后直接调用子视图的 measure 方法让子视图根据自身的 MeasureSpec 得到测量后的宽高,这个流程和之前讲解的又是一样的。

到此为止 LinearLayout 的 onMeasure 垂直方向大致的流程已经分析完毕。总结下流程:它会先遍历所有子视图,通过 LinearLayout 的 MeasureSpec 和子视图的 LayoutParams 得出子视图的 MeasureSpec,接着让子视图执行 measure 方法 ,计算子视图测量后的宽/高。通过累加子视图的高度,如果 LinearLayout 是 EXACTLY 模式那么高度还是自身的尺寸,如果 LinearLayout 是 AT_MOST 模式那么对比子视图高度总和取较小一方作为 LinearLayout 的高度。同理,宽度也有这么一个比较过程。关于 weight 属性,最关键的其实是它会让子视图 measure 两次,稍微有点耗时

举个栗子,现在有一个布局,LinearLayout 中嵌套一个 TextView 和 View 视图,以下是图解:

2. layout

layout 和 measure 的流程是类似的,直接上源码:

// ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {......host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());// 以下主要是对 requestLayout 处理,暂不深究。......
}
复制代码

host 就是 DecorView, 直接可以看到 View.layout 方法,虽说此方法没被 final 修饰,但可以看做统一入口,其他子类貌似并没有重写此方法:

public void layout(int l, int t, int r, int b) {.....boolean changed = isLayoutModeOptical(mParent) ?setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);......onLayout(changed, l, t, r, b);......
}
复制代码

先解释下前半部分的代码,这里的 l, t, r, b 分别表示 自身左边缘与父容器左边缘的距离、自身上边缘与父容器上边缘的距离、自身右边缘与父容器左边缘的距离、自身下边缘与父容器上边缘的距离,根据这些值就能得出自身的宽度为 r - l, 高度为 b - t, 以及自身的四个顶点。 这里比较重要的是 setFrame 方法,里面用全局变量 mLeft, mTop, mRight, mBottom 分别记录了 l, t, r, b, 这个时候它的宽/高算是真正的定下来了(注意 measure 阶段的测量宽高不一定是最终宽高),并且 setFrame 内部调用了, onSizeChanged 方法,于是恍然大悟,怪不得写自定义 View 的时候要在 onSizeChanged 内拿最终宽高。

接下来解释下 layout 方法中的 onLayout 方法。View 类并没有实现 onLayout,也就是说它完全去让子类去实现了,并且 ViewGroup 将此方法设为抽象方法强制去实现,因此只要是父容器都得实现 onLayout 来控制子视图的位置,而子视图没有特殊需求基本不需要去实现此方法。下面看下 LinearLayout 重写的 onLayout 方法,同样只看垂直方向:

void layoutVertical(int left, int top, int right, int bottom) {......for (int i = 0; i < count; i++) {......setChildFrame(child, childLeft, childTop + getLocationOffset(child),childWidth, childHeight);}
}
复制代码

依然还是省略了一堆代码,只需要解释关键的几个变量。 childLeft 表示子视图的左边缘与父容器的左边缘的距离,这个变量会被padding, margin, gravity 所影响。childTop 表示子视图的上边缘与父容器的上边缘的距离,受到 padding, 已累加的高度影响(因为是垂直布局)。childWidth 和 childHeight 分别是子视图的测量后的宽/高。在 setChildFrame 方法中直接调用了 child.layout, 那么 layout 事件继续往子容器传递,过程和之前解释的一样。

对 layout 做个总结:layout 方法的四个参数决定了自身在父容器内的位置保存为 mLeft, mTop, mRight, mBottom,此方法真正确定了自身的最终宽高。然后如果是继承 ViewGroup 的父容器,那么会重写 onLayout 方法对子视图进行布局确定它们的位置,最后会调用到子视图的 layout 方法,按这种步骤一直传递。

依然举个栗子,,LinearLayout 中嵌套一个 TextView 和 View 视图,以下是图解:

3. draw

performDraw 方法会调到 View 的 draw 方法,重点在于 onDraw 自身的绘制,这也是自定义 View 实现的最关键方法,其次是 dispatchDraw, 此方法在 ViewGroup 被重写主要用来遍历子视图并调用它们的 draw 方法传递绘制事件:

public void draw(Canvas canvas) {// 绘制背景drawBackground(canvas);// 绘制自身内容onDraw(canvas);// 遍历子视图让它们绘制 drawdispatchDraw(canvas);// 画装饰(前景,滚动条)onDrawForeground(canvas);// 绘制默认焦点高亮drawDefaultFocusHighlight(canvas);
}
复制代码

draw 调用流程是比较清晰简单的,但它真正的实现是很复杂的,这一块是自定义 View 的关键部分,需要学很多东西呀。。不过从这里能看出自定义 View 主要是重写 onDraw 以及 onMeasure 方法,而自定义 ViewGroup 主要是重写 onMeasure 以及 onLayout 方法。

三、总结

用文字的形式表达下整个绘制流程:

整个绘制流程的入口是 ViewRootImpl.performTravesals 方法,绘制的先后顺序是 measure, layout, draw.

performMeasure 通过计算得出 DecorView 的 MeasureSpec 然后调用其 measure 方法,此方法是 View 类的统一入口,主要是做了判断是否要测量和布局,如果需要则直接调用重写的 onMeasure 方法(因继承 ViewGroup 容器的布局特性所决定的)根据 MeasureSpec 对自身进行测量得出宽/高。父容器会遍历所有子视图,根据自身的 MeasureSpec 和 子视图的 LayoutParams 决定子视图的 MeasureSpec, 并调用子视图的 measure 方法传递测量事件,直到传递到整个 View 树的叶子为止。

performLayout 从 View 树的顶端开始,依次向下调用 layout 方法来确认自身在父容器内的位置,这时最终的宽高被确认,然后调用重写过的 onLayout 方法(根据布局特性重写)来确认所有子视图的位置。

performDraw 也是按照前面测量和布局的思路传递在整个 View 树中,onDraw 绘制自身的内容是实现自定义View的最关键方法。

View 相关的常见问题:

  • requestLayout 为什么耗时?View 调用 requestLayout 方法后,会自下而上传递事件,将设置每层 View 的测量和布局的标志位,最后会调用 performTravesals 方法基本会重新走一遍整棵 View 树的绘制流程 measure, layout, draw。
  • invalidate 和 postInvalidate?这两个重绘方法也会调用到 performTravesals, 但不会设置测量和布局的标志位,所以只会执行 draw 过程。invalidate 在主线程中执行,postInvalidate 是异步绘制,通过 handler 回调到主线程。
  • onMeasure 多次调用的情况?绘制过程中可能会出现多次 measure 的情况,如父容器 LinearLayout 使用了 weight 属性。
  • onSizeChanged 调用时机?此方法在 layout 中调用,这时已经确认了最终的宽/高,因此这个方法取宽高的时机比 onMeasure 取宽高的时机靠谱。
  • RelativeLayout 和 LinearLayout 性能对比?一般层级比较多的情况下推荐使用 RelativeLayout,因为它可以有效减少 LinearLayout 的层级问题,但只有一层的情况下推荐用 LinearLayout,因为 RelativeLayout 总是会 measure 两次,而 LinearLayout 不设置 weight 的话只会 measure 一次。RelativeLayout 中优先用 padding 而不是 margin,对margin 的处理比较耗时。
  • 还有啥问题呢。。

最后推荐 ConstraintLayout,还没有真正去研究这个约束布局,但它基本一层就能搞定一个布局,还管你什么层级的性能问题吗?应该是完爆其他布局的。

转载于:https://juejin.im/post/5c4c1ac55188252e46622d96

View 绘制流程分析相关推荐

  1. Android中View绘制流程分析

    创建Window 在Activity的attach方法中通过调用PolicyManager.makeNewWindo创建Window,将一个View add到WindowManager时,Window ...

  2. View绘制流程分析

    setContentView()做了什么 我们知道在onCreate()方法中setContentView()方法是将xml文件解析到DecorView上,那么DecorView又是什么时候显示在屏幕 ...

  3. android字符显示流程图,Android应用层View绘制流程与源码分析

    1  背景 还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原 ...

  4. Android应用层View绘制流程与源码分析

    前言 Activity中界面加载显示的基本流程原理,最终分析结果就是下面的关系: 看见没有,如上图中id为content的内容就是整个View树的结构,所以对每个具体View对象的操作,其实就是个递归 ...

  5. View绘制流程的入口

    View绘制流程的入口是WindowManager.add(decor, l),从Activity的创建开始分析,具体流程如下: Activity.onCreate() setContentView( ...

  6. android 绘图流程,Android View绘制流程

    前言 不知道大家有没有想过一个问题,当启动一个Activity的时候,相应的XML布局文件中的View是如何显示到屏幕上的?有些同学会说是通过onMeasure().onLayout().onDraw ...

  7. 【朝花夕拾】Android自定义View篇之(一)View绘制流程

    前言 转载请申明转自[https://www.cnblogs.com/andy-songwei/p/10955062.html]谢谢! 自定义View.多线程.网络,被认为是Android开发者必须牢 ...

  8. 【朝花夕拾】Android自定义View之(一)手把手教你看懂View绘制流程——向源码要答案

    前言 原文:Android自定义View之(一)手把手教你看懂View绘制流程--向源码要答案 View作为整个app的颜值担当,在Android体系中占有重要的地位.深入理解Android View ...

  9. Android UI绘制流程分析(三)measure

    源码版本Android 6.0 请参阅:http://androidxref.com/6.0.1_r10 本文目的是分析从Activity启动到走完绘制流程并显示在界面上的过程,在源码展示阶段为了使跟 ...

最新文章

  1. 基因组与数据整合:DNA应用开发正在临近
  2. 使用SAP iRPA Studio创建的本地项目,如何部署到SAP云平台上?
  3. 两种方法清除Excel保护密码
  4. 35-46集 中心极限定理、置信区间
  5. yml的基本语法,以及java读取yml文件
  6. 学习之法 —— 如何阅读代码、如何编写代码
  7. 数据库—并发调度的可串行性
  8. 华为OJ 初级:等差数列
  9. ABAP 销售订单BAPI创建批导程序
  10. 音乐源码重新上传了,请童鞋们自行下载
  11. Bug解决-RuntimeError: Sizes of tensors must match except in dimension 2. Got 320 and 160 (The offendin
  12. dos u盘测试软件,u盘DOS启动盘制作工具(BootFlashDos)
  13. UC手机浏览器(U3内核)相关文档整理
  14. python中sys.argv的用法_python的sys.argv[]用法解释
  15. 台湾大学林轩田机器学习基石课程学习笔记9 -- Linear Regression
  16. OJ 2513: 小勇学分数
  17. kmplayer android官方下载,KMPlayer下载
  18. python字符类型是英文_Python中常见数据类型
  19. 亚马逊账号关联因素有哪些?如何避免账号之间关联
  20. 人脸识别主要机算法原理

热门文章

  1. 小程序渲染html的两种方法
  2. 【Eclipse】将控制台输出直接保存到文本文件
  3. 菜鸟教程终极篇之Microsoft Windows Pre-installation Environment (Windows PE) 2.0
  4. 《进化——我们在互联网上奋斗的故事》一一1.1 靠谱工程师向管理者的转变...
  5. 基于SDN的应用定义安全方案
  6. CSS------如何让大小不一样的div中心对齐
  7. GRpc-Go使用笔记
  8. 1 第一次画PCB总结
  9. tom启动报错:org.xml.sax.SAXParseException: Content is not allowed in prolog.
  10. 部署SCCM 2012R2之一:了解功能篇