Recycleview的onMeasure过程重看

今天我又打开了Recycleview源码进行阅读。这次我深度分析了RecycleviewonMeasure方法。RecycleviewonMeasure方法里面干了很多事情,如果你的Recycleview是自动测量(LinearLayout默认就开启了自动测量),如果宽高是确定的那么他不会在onMeasure阶段进行预布局,如果是wrap_content的话,那他还要进行预布局在onMeasure阶段就要去测量孩子的大小然后在设置Recycleview的具体宽高值,这样会导致在onMeasure阶段花费更多的时间。

想要读懂这个方法我们需要深度了解void onMeasure(int widthSpec,int heightSpec)里面widthSpec和heightSpec参数值的由来。

**widthSpec 和 heightSpec**值的由来:
他是ViewGroup根据自己的长宽值然后再根据孩子的长宽值,父视图在来设定孩子你最终的长宽是多少。这里讲的有点抽像,现在根据案例还讲解:
这里我拿Linearlayout来举例:
1
2
3
4
5
6
7
8
9
10
<LinearLayout
android:layout_width="100dp"
android:layout_height="100dp"
android:orientation="vertical">

<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ff5566" />
</LinearLayout>
![截图 2022-10-12 10-47-26.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a373f225da4d4b008ad6e0fd12bcfba7~tplv-k3u1fbpfcp-watermark.image?)

这一看是不是有点纳闷,我的View长宽都设置的wrap_content,那怎么在LinearLayout里面却铺满了整个LinearLayout。只有一个实现办法:LinearLayout发现孩子的宽高是wrap_content的时候,LinearLayout就强制将孩子的宽高设定成了自己的宽高值。也就是上面提到的widthSpec和heightSpec值。现在我们来看下LinearLayout的源码:

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

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
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} 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
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = 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;

// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

源码里面明确写了,当孩子是wrap_content的时候,就将孩子的宽高设置为自己的宽高值。

现在继续看回RecycleviewonMeasure方法。

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
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
// LinearLayoutManager默认是开启的
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
// 先根据默认的设置recycleview的宽高
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
// 判断当前的recycleview的宽高设置的是不是具体值,是的话就不用进行预加载的过程
// 预加载过程就是提前执行layout过程,然后计算所需要的宽高,然后在设置给recycleiew
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}

if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// 这里就是默认设置父视图规定在没有具体设置宽高值的情况下的值
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
// 进行预备加载 加载完就能知道全部孩子需要的宽高值
dispatchLayoutStep2();

// 这里就已经拿到了孩子全部的具体宽高值,然后在设置给recycleview
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

// if RecyclerView has non-exact width and height and if there is at least one child
// which also has non-exact width & height, we have to re-measure.
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
}
}

看完Recycleview的测量过程,我认为你能够设置具体值的就最好设置具体值,不然在onMeasure厘面还要在走一比那layout过程。

这里再稍微讲下layout过程,为什么要讲呢,因为有一部分设计到了上面onMeasure的过程,当时困扰了我挺久:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
return;
}
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {// 3
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}

前面都很容易看懂,问题就在我标记的3处。

1
mLayout.getHeight() != getHeight()

现在看下mLayout.getHeight()

1
2
3
4
//LayoutManager.java
public int getHeight() {
return mHeight;
}

那这个mHeight是什么时候赋值的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// LayoutManager.java
void setMeasureSpecs(int wSpec, int hSpec) {
mWidth = MeasureSpec.getSize(wSpec);
mWidthMode = MeasureSpec.getMode(wSpec);
if (mWidthMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) {
mWidth = 0;
}

mHeight = MeasureSpec.getSize(hSpec);
mHeightMode = MeasureSpec.getMode(hSpec);
if (mHeightMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) {
mHeight = 0;
}
}

可以看到在调用方法setMeasureSpecs方法时候赋值的,那这个方法又是什么时候调用的:答案在onMeasuere方法的时候,再问具体点就是在还没有进行预加载之前进行的:可以看到标记3的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.isAutoMeasureEnabled()) {
//3
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
}

现在mLayout.getHeight搞懂了。我们再来看下getHeight()方法:

1
2
3
public final int getHeight() {
return mBottom - mTop;
}

这个getHeight是通过mBottom - mTop来获取的。其实就是我们经常获取控件高度的方式。
那现在是不是有疑问了,mLayout.getHeight获取的值也是通过测量高度设置进行的,然后getHeight()获取的值就是控件的高度那为什么这两个值会存在不相等的情况呢?

不想等的问题就出现在onMeasure里面进行了预加载处理。在预加载前给recycleview设置了他的默认宽高。但是在预加载结束后,根据具体的孩子数量算出的高度值才是recycleview真实的宽高值。然后mBottom的值在layout阶段才设置进去的。所以在layout阶段获取到的recycleview的值和在onMeasure里面预加载阶段前获取到的宽高值会存在不同的情况。