前言:
最近遇到的一个 Bug , 当我们应用将 View 生成图片后 , 布局会出现布局错乱的现象, 检查代码发现 , 之前的工程师的 ui和代码逻辑是没什么问题的 , 那问题应该就是出在生成图片的操作上面,先来看看代码
view.measure( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ) view.layout(0, 0, view.measuredWidth, view.measuredHeight) val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) view.draw(canvas)
(生成图片的代码)
整个代码的逻辑很简单,执行了一遍 view 的 measure() 、 layout () 、 draw() , 然后在 draw 中放入我们自己的画布 , 最后生成 bitmap 。
经过测试 , 问题就出现在measure() 、 layout() 上 , 先后执行这两个方法 , 导致布局的宽度和高度出现问题 , 简单来说 , 原来一个 TextView 的宽度是 match_conten , 当我们调用完这两个方法后就变成了 TextView 文字的宽度 , 而这个时候如果有其他布局依赖于这个 TextView 的位置就会导致布局出现问题
这个时候就在想 , 这三个方法其实就是 View 的绘制过程啊 , 但是在我们执行之前 , View 生成时应该是已经执行过一次的 , 所以我把 measure() 、 layout () 注释掉 , 单纯执行 draw () , 发现代码是可以正常执行的 , 同时布局没有出现问题 。
我又试了注释一个 measure() , 执行另外两个方法 , 发现也是正常运行 , 注释 layout () , 执行另外两个方法 , 发现执行也是正常的 , 那么为什么先后执行 measure() 、 layout () 布局就会有问题呢 ?
我开始思考View的绘制过程
从DecorView 被加载到 Window 开始 :
我们都知道, PhoneWindow 是 Android 系统中最基本的窗口系统,每个 Activity 会创建一个。同时, PhoneWindow 也是 Activity 和 View 系统交互的接口。 DecorView 本质上是一个 FrameLayout ,是 Activity 中所有 View 的祖先。
从 Activity 的 startActivity 开始,最终调用到 ActivityThread 的 handleLaunchActivity 方法来创建 Activity ,相关核心代码如下:
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) { .... // 创建Activity,会调用Activity的onCreate方法 // 从而完成DecorView的创建 Activity a = performLaunchActivity(r, customIntent); if (a != null) { r.createdConfig = new Configuration(mConfiguration); Bundle oldState = r.state; handleResumeActivity(r.tolen, false, r.isForward, !r.activity..mFinished && !r.startsNotResumed); } } final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { unscheduleGcIdler(); mSomeActivitiesChanged = true; // 调用Activity的onResume方法 ActivityClientRecord r = performResumeActivity(token, clearHide); if (r != null) { final Activity a = r.activity; ... if (r.window == null &&& !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); // 得到DecorView View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); // 得到了WindowManager,WindowManager是一个接口 // 并且继承了接口ViewManager ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { a.mWindowAdded = true; // WindowManager的实现类是WindowManagerImpl, // 所以实际调用的是WindowManagerImpl的addView方法 wm.addView(decor, l); } } } }
在这里面出现了两个概念 ViewRoot 和 DecorView, 应该简单的提两句 , 因为 View 的三大流程均是通过 ViewRoot 来完成的 。ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带。 当Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。
View 绘制的流程 :
在 Android 中 , View 的绘制主要是 3 大流程 , 分别是 measure , layout , draw , 分别对应着测量 , 位置 , 和绘制 。
同时,在Android 中,主要有两种视图: View 和 ViewGroup , View 就是一个独立的视图, ViewGroup 是 一个容器组件,该容器可容纳多个子视图,即ViewGroup 可容纳多个 View 或 ViewGroup ,且支持嵌套。 虽然ViewGroup 继承于View ,但是在 View 绘制三大流程中,View 和ViewGroup 它们之间的操作并不完全相同,比如:
-
View 和 ViewGroup 都需要进行 measure ,确定各自的测量宽 / 高。 View 只需直接测量自身即可,而 ViewGroup 通常都必须先测量所有子 View ,最后才能测量自己
-
通常ViewGroup 先定位自己的位置( layout ),然后再定位其子 View 位置( onLayout )
-
View 需要进行 draw 过程,而 ViewGroup 通常不需要(当然也可以进行绘制),因为 ViewGroup 更多作为容器存在,起存储放置功能
measure:
对于 View 的测量主要分为两个步骤
-
求取 View 的测量规格 MeasureSpec 。
-
依据上一步求得的MeasureSpec ,对 View 进行测量,求取得到 View 的最终测量宽 / 高。
那什么是 MeasureSpec呢?MeasureSpec 表示的是一个32 位的整形值,它的高2 位表示测量模式SpecMode ,低30 位表示某种测量模式下的规格大小SpecSize
public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0X3 << MODE_SHIFT; // 不指定测量模式, 父视图没有限制子视图的大小,子视图可以是想要 // 的任何尺寸,通常用于系统内部,应用开发中很少用到。 public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 精确测量模式,视图宽高指定为match_parent或具体数值时生效, // 表示父视图已经决定了子视图的精确大小,这种模式下View的测量 // 值就是SpecSize的值。 public static final int EXACTLY = 1 << MODE_SHIFT; // 最大值测量模式,当视图的宽高指定为wrap_content时生效,此时 // 子视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸。 public static final int AT_MOST = 2 << MODE_SHIFT; // 根据指定的大小和模式创建一个MeasureSpec public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } // 微调某个MeasureSpec的大小 static int adjust(int measureSpec, int delta) { final int mode = getMode(measureSpec); if (mode == UNSPECIFIED) { // No need to adjust size for UNSPECIFIED mode. return make MeasureSpec(0, UNSPECIFIED); } int size = getSize(measureSpec) + delta; if (size < 0) { size = 0; } return makeMeasureSpec(size, mode); } }
( MeasureSpec核心代码)
一个MeasureSpec 表达的是:该 View 在该种测量模式( SpecMode )下对应的测量尺寸( SpecSize )。其中, SpecMode 有三种类型:
-
UNSPECIFIED :表示父容器对子 View 未施加任何限制,子 View 尺寸想多大就多大。
-
EXACTLY :如果子 View 的模式为 EXACTLY ,则表示子 View 已设置了确切的测量尺寸,或者父容器已检测出子 View 所需要的确切大小。 这种模式对应于LayoutParams.MATCH_PARENT 和子View 设置具体数值两种情况。
-
AT_MOST :表示自适应内容,在该种模式下, View 的最大尺寸不能超过父容器的 SpecSize ,因此也称这种模式为 最大值模式。 这种模式对应于LayoutParams.WRAP_CONTENT 。
Measure 的基本流程 :
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... // ViewGroup没有定义测量的具体过程,因为ViewGroup是一个 // 抽象类,其测量过程的onMeasure方法需要各个子类去实现 onMeasure(widthMeasureSpec, heightMeasureSpec); ... } // 不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同,如果需要自定义测量过程,则子类可以重写这个方法 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // setMeasureDimension方法用于设置View的测量宽高 setMeasureDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } // 如果View没有重写onMeasure方法,则会默认调用getDefaultSize来获得View的宽高 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 = sepcSize; break; } return result; }
(View . measure()源码 )
View.measure(int, int) 中参数 widthMeasureSpec 和 heightMeasureSpec 是由父容器传递进来的,具体的测量过程请参考后文内容。
需要注意的是,View.measure(int, int) 是一个 final 方法,因此其不可被覆写,实际真正测量 View 自身使用的是 View.onMeasure(int, int) 方法 .
在 onMeasure 中主要做了三件事
第一件事 : 通过getSuggestedMinimumWidth()/getSuggestedMinimumHeight() 方法获取得到 View 的推荐最小测量宽 / 高
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); }
(View.java)
可以很清晰看出,当 View 没有设置背景时,它的宽度就为 mMinWidth , mMinWidth 就是 android:minWidth 这个属性对应设置的值(未设置 android:minWidth 时,其值默认为 0 ),当 View 设置了背景时,它的宽度就是 mMinWidth 和 mBackground.getMinimumWidth() 之中的较大值
getSuggestedMinimumWidth()/getSuggestedMinimumHeight() 其实就是用于获取 View 的最小测量宽 / 高,其具体逻辑为:当 View 没有设置背景时,其最小宽 / 高为 android:minWidth/android:mMinHeight 所指定的值,当 View 设置了背景时,其最小测量宽 / 高为 android:minWidth/android:minHeight 与其背景图片宽 / 高的较大值。
简而言之,View 的最小测量宽 / 高为 android:minWidth/android:minHeight 和其背景宽 / 高之间的较大值。
第二件事 : 通过getDefaultSize() 获取到 View 的默认测量宽 / 高
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; }
(getDefaultSize()源码)
这里的size 是通过 getSuggestedMinimumWidth()/getSuggestedMinimumHeight() 方法获取得到系统建议 View 的最小测量宽 / 高。
参数measureSpec 是经由 View.measure()==>View.onMeasure()==>View.getDefaultSize() 调用链传递进来的,表示的是当前 View 的 MeasureSpec 。
getDefaultSize() 内部首先会获取 View 的测量模式和测量大小,然后当 View 的测量模式为 UNSPECIFIED 时,也即未限制 View 的大小,因此此时 View 的大小就是其原生大小(也即 android:minWidth 或背景图片大小),当 View 的测量模式为 AT_MOST 或 EXACTLY 时,此时不对这两种模式进行区分,一律将 View 的大小设置为测量大小(即 SpecSize ) 。
第三件事 : 获取到 View 的测量宽 / 高后,通过 setMeasuredDimension() 记录 View 的测量宽 / 高:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { ... setMeasuredDimensionRaw(measuredWidth, measuredHeight); } // 记录测量宽/高 private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }
(setMeasuredDimension()源码)
无论是对View 的测量还是 ViewGroup 的测量,都是由 View#measure(int widthMeasureSpec, int heightMeasureSpec) 方法负责,然后真正执行 View 测量的是 View 的 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 方法。
具体来说,View 直接在 onMeasure() 中测量并设置自己的最终测量宽 / 高。在默认测量情况下, View 的测量宽 / 高由其父容器的 MeasureSpec 和自身的 LayoutParams 共同决定,当 View 自身的测量模式为 LayoutParams.UNSPECIFIED 时,其测量宽 / 高为 android:minWidth/android:minHeight 和其背景宽 / 高之间的较大值,其余情况皆为自身 MeasureSpec 指定的测量尺寸。
而对于ViewGroup 来说,由于布局特性的丰富性,只能自己手动覆写 onMeasure() 方法,实现自定义测量过程,但是总的思想都是先测量 子 View 大小,最终才能确定自己的测量大小。
layout:
当确定了 View 的测量大小后,接下来就可以来确定 View 的布局位置了,也即将 View 放置到屏幕具体哪个位置。 这也是 layout 干的事情 。
View.layout() 主要就做了两件事:
-
setFrame() :首先通过 View.setFrame() 来确定自己的布局位置
-
onLayout() : setFrame() 是用于确定 View 自身的布局位置,而 onLayout() 主要用于确定 子 View 的布局位置
protected boolean setFrame(int left, int top, int right, int bottom) { ... // Invalidate our old position invalidate(sizeChanged); mLeft = left; mTop = top; mRight = right; mBottom = bottom; }
(setFrame()源码 )
draw :
当 View 的测量大小,布局位置都确定后,就可以最终将该 View 绘制到屏幕上了。 这也就是 draw 干的事情 。
public void draw(Canvas canvas) { ... /* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */ // Step 1, draw the background, if needed drawBackground(canvas); // skip step 2 & 5 if possible (common case) ... // Step 2, save the canvas' layers if (drawTop) { canvas.saveLayer(left, top, right, top + length, null, flags); } if (drawBottom) { canvas.saveLayer(left, bottom - length, right, bottom, null, flags); } if (drawLeft) { canvas.saveLayer(left, top, left + length, bottom, null, flags); } if (drawRight) { canvas.saveLayer(right - length, top, right, bottom, null, flags); } ... // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 5, draw the fade effect and restore layers ... if (drawTop) { ... canvas.drawRect(left, top, right, top + length, p); } if (drawBottom) { ... canvas.drawRect(left, bottom - length, right, bottom, p); } if (drawLeft) { ... canvas.drawRect(left, top, left + length, bottom, p); } if (drawRight) { ... canvas.drawRect(right - length, top, right, bottom, p); } ... // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); }
(View.draw()源码)
View.draw() 主要做了以下 6 件事:
-
绘制背景:drawBackground()
-
如果有必要的话,保存画布图层:Canvas.saveLayer()
-
绘制自己:onDraw()
-
绘制子View : dispatchDraw()
-
如果有必要的话,绘制淡化效果并恢复图层:Canvas.drawRect()
-
绘制装饰:onDrawForeground()
总结:
View 的绘制主要 三大流程 :
measure :测量流程,主要负责对 View 进行测量,其核心逻辑位于 View.measure() ,真正的测量处理由 View.onMeasure() 负责。默认的测量规则为:如果 View 的布局参数为 LayoutParams.WRAP_CONTENT 或 LayoutParams.MATCH_PARENT ,那么其测量大小为 SpecSize ;如果其布局参数为 LayoutParams.UNSPECIFIED ,那么其测量大小为 android:minWidth/android:minHeight 和其背景之间的较大值。
自定义View 通常覆写 onMeasure() 方法,在其内一般会对 WRAP_CONTENT 预设一个默认值,区分 WARP_CONTENT 和 MATCH_PARENT 效果,最终完成自己的测量宽 / 高。而 ViewGroup 在 onMeasure() 方法中,通常都是先测量子 View ,收集到相应数据后,才能最终测量自己。
layout :布局流程,主要完成对 View 的位置放置,其核心逻辑位于 View.layout() ,该方法内部主要通过 View#setFrame() 记录自己的四个顶点坐标(记录与对应成员变量中即可),完成自己的位置放置,最后会回调 View.onLayout() 方法,在其内完成对 子 View 的布局放置。
( 不同于 measure 流程首先对 子 View 进行测量,最后才测量自己, layout 流程首先是先定位自己的布局位置,然后才处理放置 子 View 的布局位置。 )
draw :绘制流程,就是将 View 绘制到屏幕上,其核心逻辑位于 View#draw() ,主要就是对 背景、自身内容( onDraw() )、子 View ( dispatchDraw() )、装饰(滚动条、前景等) 进行绘制。
( 通常自定义View 覆写 onDraw() 方法,完成自己的绘制即可, ViewGroup 一般充当容器使用,因此通常无需覆写 onDraw() )
Activity 的根视图(即 DecorView )最终是绑定到 ViewRootImpl ,具体是由 ViewRootImpl#setView(...) 进行绑定关联的,后续 View 绘制的三大流程都是均有 ViewRootImpl 负责执行的。
对 View 的测量流程中,最关键的一步是求取 View 的 MeasureSpec , View 的 MeasureSpec 是在其父容器 MeasureSpec 的约束下,结合自己的 LayoutParams 共同测量得到的,具体的测量逻辑由 ViewGroup.getChildMeasureSpec() 负责。
DecorView 的 MeasureSpec 取决于自己的 LayoutParams 和屏幕尺寸,具体的测量逻辑位于 ViewRootImpl.getRootMeasureSpec() 。
用逻辑过一遍流程:
1 、 首先,当 Activity 启动时,会触发调用到 ActivityThread#handleResumeActivity() ,其内部会经历一系列过程,生成 DecorView 和 ViewRootImpl 等实例,最后通过 ViewRootImpl#setView(decor,MATCH_PARENT) 设置 Activity 根 View 。
( ViewRootImpl.setView() 内容通过将其成员属性 ViewRootImpl.mView 指向 DecorView ,完成两者之间的关联。)
2 、 ViewRootImpl 成功关联 DecorView 后,其内部会设置同步屏障并发送一个 CALLBACK_TRAVERSAL 异步渲染消息,在下一次 VSYNC 信号到来时, CALLBACK_TRAVERSAL 就会得到响应,从而最终触发执行 ViewRootImpl.performTraversals() ,真正开始执行 View 绘制流程。
3 、 ViewRootImpl.performTraversals() 内部会依次调用 ViewRootImpl#performMeasure() 、 ViewRootImpl#performLayout() 和 ViewRootImpl#performDraw() 三大绘制流程,其中:
4 、 performMeasure() :内部主要就是对 DecorView 执行测量流程: DecorView#measure() 。 DecorView 是一个 FrameLayout ,其布局特性是层叠布局,所占的空间就是其 子 View 占比最大的宽 / 高,因此其测量逻辑( onMeasure() )是先对所有 子 View 进行测量,具体是通过 ViewGroup.measureChildWithMargins(...) 方法对 子 View 进行测量,子 View 测量完成后,记录最大的宽 / 高,设置为自己的测量大小(通过 View#setMeasuredDimension() ),如此便完成了 DecorView 的测量流程。
5 、 performLayout() :内部其实就是调用 DecorView.layout() ,如此便完成了 DecorView 的布局位置,最后会回调 DecorView.onLayout() ,负责 子 View 的布局放置,核心逻辑就是计算出各个 子 View 的坐标位置,最后通过 child.layout() 完成 子 View 布局。
6、 performDraw() :内部最终调用到的是 DecorView.draw() ,该方法内部并未对绘制流程做任何修改,因此最终执行的是 View.draw() ,所以主要就是依次完成对 DecorView 的 背景、子 View ( dispatchDraw() ) 和 视图装饰(滚动条、前景等) 的绘制
回到开头 Bug , 其原因就是在重新调用 measure 后对布局的位置进行重新测量 ,因为子View已经有了真实的宽度,所以子View的宽度就沿用了真实宽度,又 调用了 layout 重新进行布局导致后续有依赖到这个子View的布局都出现了问题
解决方案有 2 个
1、 只调用 draw () 方法将当前布局绘制到画布上即可
2、 使用 view .drawToBitmap() 方法也可以直接生成 view 的图片