概述 上一篇 Android DecorView 与 Activity 绑定原理分析 分析了在调用 setContentView 之后,DecorView 是如何与 activity 关联在一起的,最后讲到了 ViewRootImpl 开始绘制的逻辑。本文接着上篇,继续往下讲,开始分析 view 的绘制流程。 上文说到了调用 performTraversals 进行绘制,由于 performTraversals 方法比较长,看一个简化版: 复制代码 // ViewRootImpl 类 private void performTraversals() { // 这个方法代码非常多,但是重点就是执行这三个方法 // 执行测量 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 执行布局(ViewGroup)中才会有 performLayout(lp, mWidth, mHeight); // 执行绘制 performDraw(); } 复制代码 其流程具体如下: View的整个绘制流程可以分为以下三个阶段: measure: 判断是否需要重新计算 View 的大小,需要的话则计算; layout: 判断是否需要重新计算 View 的位置,需要的话则计算; draw: 判断是否需要重新绘制 View,需要的话则重绘制。 MeasureSpec 在介绍绘制前,先了解下 MeasureSpec。MeasureSpec 封装了父布局传递给子布局的布局要求,它通过一个 32 位 int 类型的值来表示,该值包含了两种信息,高两位表示的是 SpecMode(测量模式),低 30 位表示的是 SpecSize(测量的具体大小)。下面通过注释的方式来分析来类: 复制代码 /** * 三种SpecMode: * 1.UNSPECIFIED * 父 ViewGroup 没有对子View施加任何约束,子 view 可以是任意大小。这种情况比较少见,主要用于系统内部多次measure的情形, * 用到的一般都是可以滚动的容器中的子view,比如ListView、GridView、RecyclerView中某些情况下的子view就是这种模式。 * 一般来说,我们不需要关注此模式。 * 2.EXACTLY * 该 view 必须使用父 ViewGroup 给其指定的尺寸。对应 match_parent 或者具体数值(比如30dp) * 3.AT_MOST * 该 View 最大可以取父ViewGroup给其指定的尺寸。对应wrap_content * * MeasureSpec使用了二进制去减少对象的分配。 */ public class MeasureSpec { // 进位大小为2的30次方(int的大小为32位,所以进位30位就是要使用int的最高位和第二高位也就是32和31位做标志位) private static final int MODE_SHIFT = 30; // 运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0) // (遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0) private static final int MODE_MASK = 0x3 << MODE_SHIFT; // 0向左进位30,就是00 00000000000(00后跟30个0) public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 1向左进位30,就是01 00000000000(01后跟30个0) public static final int EXACTLY = 1 << MODE_SHIFT; // 2向左进位30,就是10 00000000000(10后跟30个0) public static final int AT_MOST = 2 << MODE_SHIFT; /** * 根据提供的size和mode得到一个详细的测量结果 */ // 第一个return: // measureSpec = size + mode; (注意:二进制的加法,不是十进制的加法!) // 这里设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值 // 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100 // // 第二个return: // size &; ~MODE_MASK就是取size 的后30位,mode & MODE_MASK就是取mode的前两位,最后执行或运算,得出来的数字,前面2位包含代表mode,后面30位代表size public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } /** * 获得SpecMode */ // mode = measureSpec & MODE_MASK; // MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。 // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值 public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } /** * 获得SpecSize */ // size = measureSpec & ~MODE_MASK; // 原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } } 复制代码 顺便提下 MATCH_PARENT 和 WRAP_CONTENT 这两个代表的值,分别是 -1 和 -2。 复制代码 /** * Special value for the height or width requested by a View. * MATCH_PARENT means that the view wants to be as big as its parent, * minus the parent's padding, if any. Introduced in API Level 8. */ public static final int MATCH_PARENT = -1; /** * Special value for the height or width requested by a View. * WRAP_CONTENT means that the view wants to be just large enough to fit * its own internal content, taking its own padding into account. */ public static final int WRAP_CONTENT = -2; 复制代码 Decorview 尺寸的确定 在 performTraversals 中,首先是要确定 DecorView 的尺寸。只有当 DecorView 尺寸确定了,其子 View 才可以知道自己能有多大。具体是如何去确定的,可以看下面的代码: 复制代码 //Activity窗口的宽度和高度 int desiredWindowWidth; int desiredWindowHeight; ... //用来保存窗口宽度和高度,来自于全局变量mWinFrame,这个mWinFrame保存了窗口最新尺寸 Rect frame = mWinFrame; //构造方法里mFirst赋值为true,意思是第一次执行遍历吗 if (mFirst) { //是否需要重绘 mFullRedrawNeeded = true; //是否需要重新确定Layout mLayoutRequested = true; // 这里又包含两种情况:是否包括状态栏 //判断要绘制的窗口是否包含状态栏,有就去掉,然后确定要绘制的Decorview的高度和宽度 if (shouldUseDisplaySize(lp)) { // NOTE -- system code, won't try to do compat mode. Point size = new Point(); mDisplay.getRealSize(size); desiredWindowWidth = size.x; desiredWindowHeight = size.y; } else { //宽度和高度为整个屏幕的值 Configuration config = mContext.getResources().getConfiguration(); desiredWindowWidth = dipToPx(config.screenWidthDp); desiredWindowHeight = dipToPx(config.screenHeightDp); } ... else{ // 这是window的长和宽改变了的情况,需要对改变的进行数据记录 //如果不是第一次进来这个方法,它的当前宽度和高度就从之前的mWinFrame获取 desiredWindowWidth = frame.width(); desiredWindowHeight = frame.height(); /** * mWidth和mHeight是由WindowManagerService服务计算出的窗口大小, * 如果这次测量的窗口大小与这两个值不同,说明WMS单方面改变了窗口的尺寸 */ if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) { if (DEBUG_ORIENTATION) Log.v(mTag, "View " + host + " resized to: " + frame); //需要进行完整的重绘以适应新的窗口尺寸 mFullRedrawNeeded = true; //需要对控件树进行重新布局 mLayoutRequested = true; //window窗口大小改变 windowSizeMayChange = true; } } ... // 进行预测量 if (layoutRequested){ ... if (mFirst) { // 视图窗口当前是否处于触摸模式。 mAttachInfo.mInTouchMode = !mAddedTouchMode; //确保这个Window的触摸模式已经被设置 ensureTouchModeLocally(mAddedTouchMode); } else { //六个if语句,判断insects值和上一次比有什么变化,不同的话就改变insetsChanged //insects值包括了一些屏幕需要预留的区域、记录一些被遮挡的区域等信息 if (!mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets)) { insetsChanged = true; } ... // 这里有一种情况,我们在写dialog时,会手动添加布局,当设定宽高为Wrap_content时,会把屏幕的宽高进行赋值,给出尽量长的宽度 /** * 如果当前窗口的根布局的width或height被指定为 WRAP_CONTENT 时, * 比如Dialog,那我们还是给它尽量大的长宽,这里是将屏幕长宽赋值给它 */ if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) { windowSizeMayChange = true; //判断要绘制的窗口是否包含状态栏,有就去掉,然后确定要绘制的Decorview的高度和宽度 if (shouldUseDisplaySize(lp)) { // NOTE -- system code, won't try to do compat mode. Point size = new Point(); mDisplay.getRealSize(size); desiredWindowWidth = size.x; desiredWindowHeight = size.y; } else { Configuration config = res.getConfiguration(); desiredWindowWidth = dipToPx(config.screenWidthDp); desiredWindowHeight = dipToPx(config.screenHeightDp); } } } } } 复制代码 这里主要是分两步走: 如果是第一次测量,那么根据是否有状态栏,来确定是直接使用屏幕的高度,还是真正的显示区高度。 如果不是第一次,那么从 mWinFrame 获取,并和之前保存的长宽高进行比较,不相等的话就需要重新测量确定高度。 当确定了 DecorView 的具体尺寸之后,然后就会调用 measureHierarchy 来确定其 MeasureSpec : 复制代码 // Ask host how big it wants to be windowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight); 复制代码 其中 host 就是 DecorView,lp 是 wm 在添加时候传给 DecorView 的,最后两个就是刚刚确定显示宽高 ,看下方法的具体逻辑 : 复制代码 private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) { int childWidthMeasureSpec; int childHeightMeasureSpec; boolean windowSizeMayChange = false;boolean goodMeasure = false;      // 说明是 dialog if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { // On large screens, we don't want to allow dialogs to just // stretch to fill the entire width of the screen to display // one line of text. First try doing the layout at a smaller // size to see if it will fit. final DisplayMetrics packageMetrics = res.getDisplayMetrics(); res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true); int baseSize = 0;        // 获取一个基本的尺寸 if (mTmpValue.type == TypedValue.TYPE_DIMENSION) { baseSize = (int)mTmpValue.getDimension(packageMetrics); } if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": baseSize=" + baseSize + ", desiredWindowWidth=" + desiredWindowWidth);        // 如果大于基本尺寸 if (baseSize != 0 && desiredWindowWidth > baseSize) { childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured (" + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ") from width spec: " + MeasureSpec.toString(childWidthMeasureSpec) + " and height spec: " + MeasureSpec.toString(childHeightMeasureSpec));           // 判断测量是否准确 if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { goodMeasure = true; } else { // Didn't fit in that size... try expanding a bit. baseSize = (baseSize+desiredWindowWidth)/2; if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": next baseSize=" + baseSize); childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured (" + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")"); if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { if (DEBUG_DIALOG) Log.v(mTag, "Good!"); goodMeasure = true; } } } }      // 这里就是一般 DecorView 会走的逻辑 if (!goodMeasure) { childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);        // 与之前的尺寸进行对比,看看是否相等,不想等,说明尺寸可能发生了变化 if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) { windowSizeMayChange = true; } } return windowSizeMayChange; } 复制代码 上面主要主要做的就是来确定父 View 的 MeasureSpec。但是分了两种不同类型: 如果宽是 WRAP_CONTENT 类型,说明这是 dialog,会有一些针对 dialog 的处理,最终会调用 performMeasure 进行测量; 对于一般 Activity 的尺寸,会调用 getRootMeasureSpec MeasureSpec 。 下面看下 DecorView MeasureSpec 的计算方法: 复制代码 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; } 复制代码 该方法主要是根据 View 的 MeasureSpec 是根据宽高的参数来划分的。 MATCH_PARENT :精确模式,大小就是窗口的大小; WRAP_CONTENT :最大模式,大小不定,但是不能超过窗口的大小; 固定大小:精确模式,大小就是指定的具体宽高,比如100dp。 对于 DecorView 来说就是走第一个 case,到这里 DecorView 的 MeasureSpec 就确定了,从 MeasureSpec 可以得出 DecorView 的宽高的约束信息。 获取子 view 的 MeasureSpec 当父 ViewGroup 对子 View 进行测量时,会调用 View 类的 measure 方法,这是一个 final 方法,无法被重写。ViewGroup 会传入自己的 widthMeasureSpec 和 heightMeasureSpec,分别表示父 View 对子 View 的宽度和高度的一些限制条件。尤其是当 ViewGroup 是 WRAP_CONTENT 的时候,需要优先测量子 View,只有子 View 宽高确定,ViewGroup 才能确定自己到底需要多大的宽高。 当 DecorView 的 MeasureSpec 确定以后,ViewRootImpl 内部会调用 performMeasure 方法: 复制代码 private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { if (mView == null) { return; } Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try { mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } 复制代码 该方法传入的是对 DecorView 的 MeasureSpec,其中 mView 就是 DecorView 的实例,接下来看 measure() 的具体逻辑: 复制代码 /** * 调用这个方法来算出一个View应该为多大。参数为父View对其宽高的约束信息。 * 实际的测量工作在onMeasure()方法中进行 */ public final void measure(int widthMeasureS