0%

View Measure

前言

Android View 体系 Measure 流程分析,结合 View、ViewGroup 梳理 Android 中测量 View 大小的具体实现。

到底是从哪里开始了 measure ?

通过之前 Activity 为什么能显示 UI 一文我们知道,Activity 所显示的所有 View 是添加到 DecorView 中。ViewRootImpl 调用最终的 setView 之前,会之前调用 requestLayout 触发整个 View 树的测量、布局和绘制流程。也就是执行 performTraversals 方法。

测量流程

performTraversals

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
private void performTraversals() {
// cache mView since it is used so much below...

// 1. mView 就是 DecorView
final View host = mView;

if (host == null || !mAdded)
return;


WindowManager.LayoutParams lp = mWindowAttributes;

// 2. 窗口宽高
int desiredWindowWidth;
int desiredWindowHeight;

Rect frame = mWinFrame;
if (mFirst) {
mFullRedrawNeeded = true;
mLayoutRequested = true;

final Configuration config = mContext.getResources().getConfiguration();
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 if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
// For wrap content, we have to remeasure later on anyways. Use size consistent with
// below so we get best use of the measure cache.
desiredWindowWidth = dipToPx(config.screenWidthDp);
desiredWindowHeight = dipToPx(config.screenHeightDp);
} else {
// After addToDisplay, the frame contains the frameHint from window manager, which
// for most windows is going to be the same size as the result of relayoutWindow.
// Using this here allows us to avoid remeasuring after relayoutWindow
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
}


} else {

}



boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {

final Resources res = mView.getContext().getResources();

if (mFirst) {
// make sure touch mode code executes by setting cached value
// to opposite of the added touch mode.
mAttachInfo.mInTouchMode = !mAddedTouchMode;
ensureTouchModeLocally(mAddedTouchMode);
} else {
if (!mPendingDisplayCutout.equals(mAttachInfo.mDisplayCutout)) {
cutoutChanged = true;
}
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
windowSizeMayChange = true;

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);
}
}
}

// Ask host how big it wants to be
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}

最终调用 measureHierarchy 方法。这里可以看一下他的几个参数

  • host 其实就是 DecorView ,就是 ViewRootImpl.setView 方法添加进来的 DecorView .
  • lp 是 mWindowAttributes ,其实就是 DecorView 的对应的 WindowManager.LayoutParmas.
  • res 及 context.getResources().
  • desiredWindowWidth, desiredWindowHeight 代表屏幕的宽高

measureHierarchy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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;

if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(mTag,
"Measuring " + host + " in display " + desiredWindowWidth
+ "x" + desiredWindowHeight + "...");

boolean goodMeasure = false;
// unnormal-logic

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;
}

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;
}

在 measureHierarchy 中会根据 DecorView 的 LayoutParams 和 窗口宽高计算出 MeasureSpec 。具体实现在 getRootMeasureSpec 中。逻辑很简,就是根据屏幕宽高和测量规格再次计算封装一个 MeasureSepc。

最终会调用 performMeasure 方法。

performMeasure

1
2
3
4
5
6
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

measure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
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);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

resolveRtlPropertiesIfNeeded();

int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}

mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}

mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;

mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

measure 整体流程还是比较简单,首先会根据父布局传递的 MeasureSpec 计算一个 key ,用这个 key 作为实现计算结果缓存的效果。这也是现在常用的一种代码优化实现方式,先去缓存里查,有的话直接使用。没有的话在计算,并且存起来。

最终在需要重新测量 的前提下,如果缓存中拿不到值,那么就执行 onMeasure 方法进行测量。

onMeasure

1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

看起来很简单?只要拿到当前 view 的默认大小就可以了。

注意!注意!注意!

这里就是比较有意思的地方了,上面提到的所有方法要么是私有的,要么是 final 的。因此,调用时机和调用的对象都是一致的。但到了 onMeasure 就不一样了,他是一个 protected 的方法,他的子类是可以覆写甚至完全覆盖 view 中的实现的。

同时按照 Java 多态中方法调用的规则,这个时候实际执行的是当前 mView 对象的 onMeasure 方法。而 mView 是 DecorView (即 FrameLayout 的 onMeasure)。 这一点通过打断点在调用栈也可以很简单的看出来。

FrameLayout.onMeasure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();

int maxHeight = 0;
int maxWidth = 0;
int childState = 0;

for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
}
}

// else logic

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}

measureChildWithMargins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec = 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);


}

可以看到,在 measureChildWithMargins 方法中会根据当前 ViewGroup 自身的 padding, view 的 margin 以及 view 自己的 LayoutParams 共同计算出一个新的 MeasureSpec,然后调用 子view 的 measure 方法继续递归的执行这个过程,直到没有子 View 本身不包含 view 为止。

整体流程图

ViewGroup 中 setMeasuredDimension 会根据 LinearLayout/RelativeLayout 等不同的特性,程序不同的实现逻辑。但整体是思路是一致的。

小结

至此,我们了解了 View measure 的整体流程。

通过 WindowManager 将 DecorView 添加到 ViewRootImpl 的过程中,会主动调用 requestLayout 。在这个过程中会触发 measureHierarchy() 方法,开始整个 ViewTree 的绘制流程。

在绘制流程中,不同类型的 View 会经历不同的流程。不包括子view的 view 只负责自身大小的测量,同时一定要调用 setMeasuredDimension 方法保存大小。包含子view 的 ViewGroup 类型的 View ,同时会测量所有 view 的大小,一般是使用 measureChildWithMargins 方法。根据具体执行结果,计算自身的宽高并通过 setMeasuredDimension 保存。

哪些场景会触发 view 的测量 ?

调用 view.requestLayout 一定会触发 View 的测量过程吗?

我们可以看一下 requestLayout 的实现。

View.requestLayout()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public void requestLayout() {
// 在 view 的 measure 方法中会创建这个 mMeasureCache ,实现 measure 缓存策略
if (mMeasureCache != null) mMeasureCache.clear();

// 根据 attachInfo 信息确定,当前 view 是发起 requestLayout 的 view
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
// ViewRootImpl 正在进行 layout 操作
if (!viewRoot.requestLayoutDuringLayout(this)) {
// 在 layout 的过程中,如果需要调用了 requestLayout ,那么就进行不继续进行了,
// 而是在 layout 阶段进行一次完整的 measure/layout 过程。
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
// 标志位写 1
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;

if (mParent != null && !mParent.isLayoutRequested()) {
// 递归的向上调用
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}

/**
* Called by {@link android.view.View#requestLayout()} if the view hierarchy is currently
* undergoing a layout pass. requestLayout() should not generally be called during layout,
* unless the container hierarchy knows what it is doing (i.e., it is fine as long as
* all children in that container hierarchy are measured and laid out at the end of the layout
* pass for that container). If requestLayout() is called anyway, we handle it correctly
* by registering all requesters during a frame as it proceeds. At the end of the frame,
* we check all of those views to see if any still have pending layout requests, which
* indicates that they were not correctly handled by their container hierarchy. If that is
* the case, we clear all such flags in the tree, to remove the buggy flag state that leads
* to blank containers, and force a second request/measure/layout pass in this frame. If
* more requestLayout() calls are received during that second layout pass, we post those
* requests to the next frame to avoid possible infinite loops.
*
* <p>The return value from this method indicates whether the request should proceed
* (if it is a request during the first layout pass) or should be skipped and posted to the
* next frame (if it is a request during the second layout pass).</p>
*
* @param view the view that requested the layout.
*
* @return true if request should proceed, false otherwise.
*/
boolean requestLayoutDuringLayout(final View view) {
if (view.mParent == null || view.mAttachInfo == null) {
// Would not normally trigger another layout, so just let it pass through as usual
return true;
}
// 将当前 view 添加到这个集合中
if (!mLayoutRequesters.contains(view)) {
mLayoutRequesters.add(view);
}
// 在 performLayout 阶段,mLayoutRequesters 中需要测量的 view 的数量大于 0,那么这个值
// 会设置为 true ,其余情况均是 false
if (!mHandlingLayoutInLayoutRequest) {
// Let the request proceed normally; it will be processed in a second layout pass
// if necessary
return true;
} else {
// Don't let the request proceed during the second layout pass.
// It will post to the next frame instead.
return false;
}
}

在调用 requestLayout 的时候,系统会兼容处理恰好在 layout 阶段调用的这种情况。具体处理逻辑 requestLayoutDuringLayout 的注释已经很清楚了。

正常情况下会设置当前 view 的 PFLAG_FORCE_LAYOUTPFLAG_INVALIDATED 这两个标志位。并依次递归的向上调用 requestLayout。直达到达最顶层的 parent
ViewRootImpl. 这样就又回到了一个完整的测量流程。

那么为什么要设置这两个标志位呢?各自又有什么用呢?以及什么时候去除这个标志位? 我们可以从他们被使用的情况做一下总结。

PFLAG_FORCE_LAYOUT 的意义
  • measure 阶段

    从上面 measure 方法执行的过程可以看到 final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ,有这个标志位的话,那么会忽略缓存,进行一次完成的 onMeasure() 的调用。

  • 清除

    1
    2
    3
    layout() {
    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    }

    在 layout 结束,准确来说是在 onLayout 执行完之后,清除标志位。

  • isLayoutRequested

    1
    2
    3
    4
    5
    6
    7
    8
    9
      /**
    * <p>Indicates whether or not this view's layout will be requested during
    * the next hierarchy layout pass.</p>
    *
    * @return true if the layout will be forced during next layout pass
    */
    public boolean isLayoutRequested() {
    return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    }
  • isLayoutValid

    1
    2
    3
    4
    5
    6
     /**
    * @return {@code true} if laid-out and not about to do another layout.
    */
    boolean isLayoutValid() {
    return isLaidOut() && ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == 0);
    }

PFLAG_INVALIDATED 是和绘制相关的,这里就暂时不展开了,等分析 draw 流程的时候再看,这里只要记住在 requestLayout 的时候设置了这个标志位即可。

总结

可以看到 View measure 的细节特别多,相比 layout 和 draw 只会执行一次,measure 会由于 ViewGroup 自身的实现及子 view 的 LayoutParams 等因素执行多次。因此会使得整个流程更加的复杂,每一次去阅读源码都会看到一些新的内容。所以,View Measure 的了解绝非到此为止,在之后使用的过程中可以不断挖掘更多的内容。

加个鸡腿呗.