上一篇文章从Android程序启动过程讲解了Activity、PhoneWindow以及ViewRoot与DecorView的联系。本篇文章详细讲述一下DecorView的measure过程。
在了解measure过程过程之前需要先了解MeasureSpec这个类, MeasureSpec是一个32位的int值,高2位表示SpecMode(测量模式),低30位表示SpecSize(测量大小)。MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为方便操作,提供了打包和解包方法。MeasureSpec很大程度上决定一个View的尺寸规格,之所以说是很大程度而不是完全决定,是因为View的measure过程还受到父容器的影响。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec来测量View的宽高。MeasureSpec部分源代码如下所示。
//View.java 24515行
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
如上代码,SpecMode有三种不同的值,分别是UNSPECIFIED,EXACTLY ,AT_MOST 。这里特别指出EXACTLY的值是数字1左移30位结果1073741824,AT_MOST的值数字2左移30位结果是-2147483648。为啥特别指出这两个值,在后面代码会讲到。在UNSPECIFIED这种模式下,父容器不限制View的大小,要多大给多大,但这种情况一般用户系统内部,对于我们的开发工作几乎使用不到,所以不需深入了解。EXACTLY模式是精确模式,在这种模式下,父容器检测出View的精确大小,View的最终大小就是SpecSize的值。AT_MOST模式,在这种模式下,父容器给定一个SpecSize的值,View的大小不能大于这个值,具体多大需要看View的具体实现。
了解完MeasureSpec之后,我们开始进入DecorView的measure过程。前一篇文章说到:在ViewRootImpl中有一个方法performTraversals,在这个方法中,DecorView进行了measure,layout,draw三个过程,分别对应performMeasure,performLayout和performDraw三个方法。在performTraversals方法中调用方法performMeasure,这个方法的代码如下
//ViewRootImpl.java 2404行
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);
}
}
可以看到,它实际上是调用了mView的measure方法,前一篇文章也说明了,这个mView就是DecorView。因为measure方法是final类型,所以最终调用的是View中的measure方法,而View中的measure方法会调用实现类的onMeasure中的方法,也就是DecorView的onMeasure。所以DecorView的onMeasure方法获得的参数就是performMeasure方法的参数。这个参数是什么呢?来看下面代码。
//Build.java 576行
public static final int JELLY_BEAN_MR1 = 17;
//View.java 4545行
sUseBrokenMakeMeasureSpec = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;
//View.java 24563行
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {//判断SDK版本是否17以下
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
先来看makeMeasureSpec方法的实现,这里是将测量模式mode和测量大小size结合起来组成一个32位的int型数值。这里需要注意,测量模式mode都是左移了30位的,具体参考上文的MeasureSpec部分。方法对SDK版本是否在17上下做了区分,原因我看了一下注释,好像是因为SDK17以下使用两个值相加的话,会使RelativeLayout在滚动布局中(ScrollView或者HorizontalScrollView)出现错误。因为SDK17以上,使用了位运算来获得MeasureSpec。
//ViewRootImpl.java 375行
final Rect mWinFrame; // frame given by window manager.
//ViewRootImpl.java 1628行
Rect frame = mWinFrame;
//ViewRootImpl.java 2070行
// !!FIXME!! This next section handles the case where we did not get the
// window size we asked for. We should avoid this by getting a maximum size from
// the window session beforehand.
if (mWidth != frame.width() || mHeight != frame.height()) {
mWidth = frame.width();
mHeight = frame.height();
}
//ViewRootImpl.java 1594行
WindowManager.LayoutParams lp = mWindowAttributes;
//ViewRootImpl.java 2145行
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
//ViewRootImpl.java 2155行
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
上方的代码,performMeasure方法的参数来自getRootMeasureSpec,getRootMeasureSpec方法的参数,我已经在上方代码中,展示了他们的来源,mWidth和mHeight是mWinFrame的宽高,结合注释可以知道,这也就是窗口的大小了。lp的话,没有翻到最终来源,但是也很容易猜出,这应该是给予DecorView的布局约束。由此,DecorView的测量规格值,是有窗口大小和系统给予它的布局约束WindowManager.LayoutParams来共同决定了。继续看getRootMeasureSpec方法的源代码。
//ViewRootImpl.java 2633行
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;
}
在getRootMeasureSpec方法中,很直观的可以看到,它是根据系统给到DecorView的布局约束WindowManager.LayoutParams的值来确定DecorView的测量规格值MeasureSpec的。这里需要额外记住一点的是ViewGroup.LayoutParams.MATCH_PARENT的值是-1,ViewGroup.LayoutParams.WRAP_CONTENT的值是-2。
- 如果布局约束LayoutParams中,width或者height的值是LayoutParams.MATCH_PARENT,那么测量模式就是EXACTLY,精确模式,大小是窗口的宽度或高度;
- 如果布局约束LayoutParams中,width或者height的值是LayoutParams.WRAP_CONTENT,那么测量模式就是AT_MOST,最大模式,大小是窗口的宽度或高度;
- 如果布局约束LayoutParams中,width或者height的值是具体的数值,那么测量模式就是EXACTLY,精确模式,宽度或高度大小给定的具体大小;
至此,DecorView的onMeasure方法参数的来源就清楚了,其实就是DecorView的宽高的测量规格值。查看DecorView的onMeasure方法源代码得知,因为DecorView和子元素之间存在一些需要处理的间距,所以真正传递给子元素的测量规格值需要进行处理,这就是onMeasure方法的功能。DecorView的onMeasure方法还调用super.onMeasure(widthMeasureSpec, heightMeasureSpec);。因为DecorView继承自FrameLayout,所以这里将调用FrameLayout的onMeasure方法,在这个方法中,会获取子元素的个数,然后遍历子元素的measure过程,这样就把measure过程从DecorView传到了子元素去。而子元素的measure过程,如果这个子元素不是ViewGroup,而是普通的View,那么就执行自己的measure过程就行了,如果是ViewGroup的话,还需要像FrameLayout一样,循环调用子元素的measure过程,如此一级一级往下,所有View的measure过程都会被执行。这个在下篇文章中将会详细讲解。
DecorView的Measure过程到这就完成了。