讲一下什么情况下RecyclerView回收复用会失效。
整体涉及链路
首先第一层RecyclerView,测量子View的地方,在linearLayoutManager的onLayoutChildren()
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
mLayoutState.mInfinite = resolveIsInfinite();
...
if (mAnchorInfo.mLayoutFromEnd) {
...
fill(recycler, mLayoutState, state, false);
...
}
}
//resolveIsInfinite()开始
boolean resolveIsInfinite() {
return mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED
&& mOrientationHelper.getEnd() == 0;
}
public static OrientationHelper createHorizontalHelper(RecyclerView.LayoutManager layoutManager) {
return new OrientationHelper(layoutManager) {
public int getEndAfterPadding() {
return this.mLayoutManager.getHeight() - this.mLayoutManager.getPaddingBottom();
}
public int getEnd() {
return this.mLayoutManager.getHeight();
}
}
}
//resolveIsInfinite结束
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
...
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
}
...
}
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
...
measureChildWithMargins(view, 0, 0);
....
}
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom()
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
// 下面这个方法很关键,就不省略了。
public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
int childDimension, boolean canScroll) {
int size = Math.max(0, parentSize - padding);
int resultSize = 0;
int resultMode = 0;
if (canScroll) {
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
switch (parentMode) {
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
resultSize = size;
resultMode = parentMode;
break;
case MeasureSpec.UNSPECIFIED:
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
break;
}
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
} else {
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = parentMode;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) {
resultMode = MeasureSpec.AT_MOST;
} else {
resultMode = MeasureSpec.UNSPECIFIED;
}
}
}
//noinspection WrongConstant
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
把所有链路都列出来了,方便大家看完整个分析后,可以回过头来自己总结一遍。
从终点开始往上回溯
下面开始分析。
可以看出决定RV和子类测量模式的核心链路是
getChildMeasureSpec(getHeight(), getHeightMode(),getPaddingTop() + getPaddingBottom())
现在要找到getHeight()和getHeightMode()。
mHeight一共有四个地方赋值
void setRecyclerView(RecyclerView recyclerView) {
if (recyclerView == null) {
mRecyclerView = null;
mChildHelper = null;
mWidth = 0;
mHeight = 0;
} else {
mRecyclerView = recyclerView;
mChildHelper = recyclerView.mChildHelper;
mWidth = recyclerView.getWidth();
mHeight = recyclerView.getHeight();
}
mWidthMode = MeasureSpec.EXACTLY;
mHeightMode = MeasureSpec.EXACTLY;
}
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;
}
}
setRecyclerView先不说,setMeasureSpecs在onMeasure中,早于dispatchLayoutStep2(),也就是早于我们上面的分析的起点。
protected void onMeasure(int widthSpec, int heightSpec) {
...
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();//这里是前面所有分析的起点。
...
}
再来详细分析setMeasureSpecs。
mHeight = MeasureSpec.getSize(hSpec);
mHeightMode = MeasureSpec.getMode(hSpec);
if (mHeightMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) {
mHeight = 0;
}
可以看出mHeight取决于hSpec,hSpec是onMeasure的heightSpec,那么这个heightSpec是多少呢?
分析父View的heightSpec
我们分别看下,ViewRoorImpl,FrameLayout(也就是DecorView),LinearLayout等作为父View,RV作为子View时的heightSpec。
#ViewRootImpl
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
...
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
//这里的lp是Window的LayoutParams,所以是Match_Parent
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
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(desiredWindowHeight, lp.height);lp是Window的LayoutParams。
所以childHeightMeasureSpec从getRootMeasureSpec走的是MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY)
而windowSize就是通过DisPlay获取的。
Rect frame = mWinFrame;
if (mFirst) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
final Configuration config = 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();
}
}
所以,windowSize就是window的大小。所以childHeightMeasureSpec前两位是MeasureSpec.EXACTLY,后30位是window的真实高度。
再来看FrameLayout
#FrameLayout
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
...
}
#ViewGroup
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);
}
#ViewGroup
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 them 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);
}
由上面可知,getChildMeasureSpec方法中的spec,ViewRootImpl传给DecorView(就是FrameLayout)的heightMeasureSpec就是EXACTLY,而childDimension是LayoutParams.MATCH_PARENT,所以走的是
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
其中size就是ViewRootImpl的windowSize。
OK,父View的heightSpec就分析完了。
RecyclerView测量核心方法
再回到RecyclerView的setMeasureSpecs方法的heightSpec
所以,第一层RecyclerView,mHeight是window的size,不为0。
再看RecyclerView计算子View数量的核心方法,在fill中。
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
...
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
}
...
}
layoutState.mInfinite的赋值在onLayoutChildren的resolveIsInfinite()中
boolean resolveIsInfinite() {
return mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED
&& mOrientationHelper.getEnd() == 0;
}
public static OrientationHelper createHorizontalHelper(RecyclerView.LayoutManager layoutManager) {
return new OrientationHelper(layoutManager) {
public int getEndAfterPadding() {
return this.mLayoutManager.getHeight() - this.mLayoutManager.getPaddingBottom();
}
public int getEnd() {
return this.mLayoutManager.getHeight();//重点在这里
}
}
}
有上面函数可以看出,在getHeight()返回0的时候,并且mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED,layoutState.mInfinite为true,会一直测量下去直到数据完。
所以,避免复用失效的核心就在于,确保下面两个不能同时满足
- RecyclerView的getHeight()为0,
- SpecMode为UNSPECIFIED。
有了上面铺垫,我们可以开始正式分析复用失效场景了。
失效场景:
核心在:
public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
int childDimension, boolean canScroll) {
int size = Math.max(0, parentSize - padding);
int resultSize = 0;
int resultMode = 0;
if (canScroll) {
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
switch (parentMode) {
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
resultSize = size;
resultMode = parentMode;
break;
case MeasureSpec.UNSPECIFIED: //关注点A
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
break;
}
} else if (childDimension == LayoutParams.WRAP_CONTENT) {//关注点B
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
}
}
- 关注点A;如果子View是MATCH_PARENT,父View是UNSPECIFIED,那么满足了上面的失效场景。
- 关注点B:如果子View是WRAP_CONTENT,那么也满足了上面的失效场景。
关注点A的失效场景:
先分析关注点1,什么时候父View是UNSPECFIED呢?
回到上面父View的heightSpec,即getChildMeasureSpec()。这个是公认的方法,一般的VIewGroup都会调用这个方法,想自己定义一套行的逻辑也行,那就要全部重写(包括现有的FrameLayout,Relativelayout)几乎不可能。
这个方法里面得到UNSPECFIED要求parentSpecMode为UNSPECIFIED,并且子View是MATCH_PARENT或者WRAP_CONTENT。
那么什么情况parentSpecMode是UNSPECIFIED呢?
答案就是,所有支持滑动的空间,都支持,例如NestedScrollView,ScrollView和RecyclerView。
NestedScrollView:
#NestedScrollView
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
ViewGroup.LayoutParams lp = child.getLayoutParams();
int childWidthMeasureSpec;
int childHeightMeasureSpec;
childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft()
+ getPaddingRight(), lp.width);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
可以看到MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);满足条件。
ScrollView
#ScrollView
@Override
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
ViewGroup.LayoutParams lp = child.getLayoutParams();
int childWidthMeasureSpec;
int childHeightMeasureSpec;
childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft
+ mPaddingRight, lp.width);
final int verticalPadding = mPaddingTop + mPaddingBottom;
childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - verticalPadding),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
ScrollView同理。
关注点B的失效场景:
RecyclerView
核心同样在RecyclerView#getChildMeasureSpec中
// 下面这个方法很关键,就不省略了。
public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
int childDimension, boolean canScroll) {
int size = Math.max(0, parentSize - padding);
int resultSize = 0;
int resultMode = 0;
if (canScroll) {
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
switch (parentMode) {
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
resultSize = size;
resultMode = parentMode;
break;
case MeasureSpec.UNSPECIFIED:
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
break;
}
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
} else {
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = parentMode;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) {
resultMode = MeasureSpec.AT_MOST;
} else {
resultMode = MeasureSpec.UNSPECIFIED;
}
}
}
//noinspection WrongConstant
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
在父View是RecyclerView,不管heightMode是什么,并且子View childRecyclerView的HeightMode是WRAP_CONTENT,那childRecyclerView就满足了这个条件。
注意,childRecyclerView为指定match_parent或者wrap_content或者指定数值都没问题。