本文打算从几个地方说起。
- 将
RecyclerView
当成一个普通的View
,分别分析它的三大流程、事件传递(包括嵌套滑动) - 分析
RecyclerView
的缓存原理,这也是RecyclerView
的精华所在 - 分析
RecyclerView
的Adapter
、LayoutManager
、ItemAnimator
和ItemDecoration
RecyclerView
的扩展,包括LayoutManager
的自定义和使用RecyclerView
常见的坑
一.Recycleview绘制三大流程:
1.概述:
RecyclerView
本身是一个展示大量数据的控件,相比较ListView
,RecyclerView
的4级缓存(也有人说是3级缓存,这些都不重要)就表现的非常出色,在性能方面相比于ListView
提升了不少。同时由于LayoutManager
的存在,让RecyclerView
不仅有ListView
的特点,同时兼有GridView
的特点。这可能是RecyclerView
受欢迎的原因之一吧。
RecyclerView在设计方面上也是非常的灵活,不同的部分承担着不同的职责。其中Adapter负责提供数据,包括创建ViewHolder和绑定数据,LayoutManager负责ItemView的测量和布局,ItemAnimator负责每个ItemView
的动画,ItemDecoration
负责每个ItemView
的间隙。这种插拔式的架构使得RecyclerView
变得非常的灵活,每一个人都可以根据自身的需求来定义不同的部分。
本文RecyclerView
源码均来自于27.1.1
2.measure
来看看RecyclerView
的onMeasure
方法。
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
// 第一种情况
}
if (mLayout.isAutoMeasureEnabled()) {
// 第二种情况
} else {// 第三种情况}
}
onMeasure
方法还是有点长,这里我将它分为3种情况,我将简单解释这三种情况。
mLayout
即LayoutManager
的对象。我们知道,当RecyclerView
的LayoutManager
为空时,RecyclerView
不能显示任何的数据,在这里我们找到答案。LayoutManager
开启了自动测量时,这是一种情况。在这种情况下,有可能会测量两次。- 第三种情况就是没有开启自动测量的情况,这种情况比较少,因为为了
RecyclerView
支持warp_content
属性,系统提供的LayoutManager
都开启自动测量的,不过我们还是要分析的。
(1)当LayoutManager为空时:
这种情况下比较简单:
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
void defaultOnMeasure(int widthSpec, int heightSpec) {
// calling LayoutManager here is not pretty but that API is already public and it is better
// than creating another method since this is internal.
final int width = LayoutManager.chooseSize(widthSpec,
getPaddingLeft() + getPaddingRight(),
ViewCompat.getMinimumWidth(this));
final int height = LayoutManager.chooseSize(heightSpec,
getPaddingTop() + getPaddingBottom(),
ViewCompat.getMinimumHeight(this));
setMeasuredDimension(width, height);
}
在defaultOnMeasure
方法里面,先是通过LayoutManager
的chooseSize
方法来计算值,然后就是setMeasuredDimension
方法来设置宽高。
public static int chooseSize(int spec, int desired, int min) {
final int mode = View.MeasureSpec.getMode(spec);
final int size = View.MeasureSpec.getSize(spec);
switch (mode) {
case View.MeasureSpec.EXACTLY:
return size;
case View.MeasureSpec.AT_MOST:
return Math.min(size, Math.max(desired, min));
case View.MeasureSpec.UNSPECIFIED:
default:
return Math.max(desired, min);
}
}
chooseSize
方法表达的意思比较简单,就是通过RecyclerView
的测量mode来获取不同的值,测量模式分为三种,就是我们常见的view绘制中measure的三种模式。
到此,第一种情况就分析完毕了。因为当LayoutManager
为空时,那么当RecyclerView
处于onLayout
阶段时,会调用dispatchLayout
方法。而在dispatchLayout
方法里面有这么一行代码:
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
return;
}
所以,当LayoutManager
为空时,不显示任何数据是理所当然的。
(2)当LayoutManager开启了自动测量
在分析这种情况之前,我们得先了解几个东西。
RecyclerView
的测量分为两步,分别调用dispatchLayoutStep1
和dispatchLayoutStep2
。同时,了解过RecyclerView
源码的同学应该知道在RecyclerView
的源码里面还一个dispatchLayoutStep3
方法。这三个方法的方法名比较接近,所以容易让人搞混淆。本文会详细的讲解这三个方法的作用。
由于在这种情况下,只会调用dispatchLayoutStep1
和dispatchLayoutStep2
这两个方法,所以这里会重点的讲解这两个方法。而dispatchLayoutStep3
方法的调用在RecyclerView
的onLayout
方法里面,所以在后面分析onLayout
方法时再来看dispatchLayoutStep3
方法。
我们在分析之前,先来看一个东西--mState.mLayoutStep
。这个变量有几个取值情况。我们分别来看看:
取值 | 含义 |
---|---|
State.STEP_START |
|
State.STEP_LAYOUT | 当 |
State.STEP_ANIMATIONS | 当 |
从上表中,我们了解到mState.mLayoutStep
的三个状态对应着不同的dispatchLayoutStep
方法。这一点,我们必须清楚,否则接下来的代码将难以理解。
好的,现在开始正式分析源码:
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
/**
* This specific call should be considered deprecated and replaced with
* {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could
* break existing third party code but all documentation directs developers to not
* override {@link LayoutManager#onMeasure(int, int)} when
* {@link LayoutManager#isAutoMeasureEnabled()} returns true.
*/
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
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();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
}
我将这段代码分为三步:
- 调用
LayoutManager
的onMeasure
方法进行测量。对于onMeasure
方法,我也感觉到非常的迷惑,发现传统的LayoutManager
都没有实现这个方法。后面,我们会将简单的看一下这个方法。 - 如果
mState.mLayoutStep
为State.STEP_START
的话,那么就会执行dispatchLayoutStep1
方法,然后会执行dispatchLayoutStep2
方法。 - 如果需要第二次测量的话,会再一次调用
dispatchLayoutStep2
方法。
首先,我们来看看第一步,也就是看看onMeasure
方法。LayoutManager
的onMeasure
方法究竟为我们做什么,我们来看看:
public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}
默认是调用的RecyclerView
的defaultOnMeasure
方法,至于defaultOnMeasure
方法里面究竟做了什么,这在前面已经介绍过了,这里就不再介绍了。
View
的onMeasure
方法的作用通常来说有两个。一是测量自身的宽高,从RecyclerView
来看,它将自己的测量工作托管给了LayoutManager
的onMeasure
方法。所以,我们在自定义LayoutManager
时,需要注意onMeasure
方法存在,不过从官方提供的几个LayoutManager
,都没有重写这个方法。所以不到万得已,最好不要重写LayoutManager
的onMeasure
方法;二是测量子View
,不过到这里我们还没有看到具体的实现。
接下来,我们来分析第二步,看看dispatchLayoutStep1
方法和dispatchLayoutStep2
方法究竟做了什么。
在正式分析第二步之前,我们先对这三个方法有一个大概的认识。
方法名 | 作用 |
---|---|
dispatchLayoutStep1 | 三大 |
dispatchLayoutStep2 | 三大dispatchLayoutStep 方法第二步。在这个方法里面,真正进行children 的测量和布局。 |
dispatchLayoutStep3 | 三大 |
我们回到recycleview的onMeasure方法里面,先看看整个执行过程。
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
如果mState.mLayoutStep == State.STEP_START
时,才会调用 dispatchLayoutStep1
方法,这里与我们前面介绍mLayoutStep
对应起来了。现在我们看看dispatchLayoutStep1
方法:
private void dispatchLayoutStep1() {
mState.assertLayoutStep(State.STEP_START);
fillRemainingScrollValues(mState);
mState.mIsMeasuring = false;
startInterceptRequestLayout();
mViewInfoStore.clear();
onEnterLayoutOrScroll();
processAdapterUpdatesAndSetAnimationFlags();
saveFocusInfo();
mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
mItemsAddedOrRemoved = mItemsChanged = false;
mState.mInPreLayout = mState.mRunPredictiveAnimations;
mState.mItemCount = mAdapter.getItemCount();
findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
if (mState.mRunSimpleAnimations) {
// 找到没有被remove的ItemView,保存OldViewHolder信息,准备预布局
}
if (mState.mRunPredictiveAnimations) {
// 进行预布局
} else {
clearOldPositions();
}
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
mState.mLayoutStep = State.STEP_LAYOUT;
}
这里先简单分析一下这个方法,因为这个方法跟ItemAnimator
有莫大的关系,后续在介绍ItemAnimator
时会详细的分析。在这里,我们将重点放在processAdapterUpdatesAndSetAnimationFlags
里面,因为这个方法计算了mRunSimpleAnimations
和mRunPredictiveAnimations
。
private void processAdapterUpdatesAndSetAnimationFlags() {
if (mDataSetHasChangedAfterLayout) {
// Processing these items have no value since data set changed unexpectedly.
// Instead, we just reset it.
mAdapterHelper.reset();
if (mDispatchItemsChangedEvent) {
mLayout.onItemsChanged(this);
}
}
// simple animations are a subset of advanced animations (which will cause a
// pre-layout step)
// If layout supports predictive animations, pre-process to decide if we want to run them
if (predictiveItemAnimationsEnabled()) {
mAdapterHelper.preProcess();
} else {
mAdapterHelper.consumeUpdatesInOnePass();
}
boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
mState.mRunSimpleAnimations = mFirstLayoutComplete
&& mItemAnimator != null
&& (mDataSetHasChangedAfterLayout
|| animationTypeSupported
|| mLayout.mRequestedSimpleAnimations)
&& (!mDataSetHasChangedAfterLayout
|| mAdapter.hasStableIds());
mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
&& animationTypeSupported
&& !mDataSetHasChangedAfterLayout
&& predictiveItemAnimationsEnabled();
}
这里我们的重心放在mFirstLayoutComplete
变量里面,我们发现mRunSimpleAnimations
的值与mFirstLayoutComplete
有关,mRunPredictiveAnimations
同时跟mRunSimpleAnimations
有关。所以这里我们可以得出一个结论,当RecyclerView
第一次加载数据时,是不会执行的动画。换句话说,每个ItemView
还没有layout
完毕,怎么会进行动画。
接下来我们看看dispatchLayoutStep2
方法,这个方法是真正布局children
。我们来看看:
private void dispatchLayoutStep2() {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mPendingSavedState = null;
// onLayoutChildren may have caused client code to disable item animations; re-check
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
}
在这里,我们重点的看两行代码。一是在这里,我们可以看到Adapter
的getItemCount
方法被调用;二是调用了LayoutManager
的onLayoutChildren
方法,这个方法里面进行对children
的测量和布局,同时这个方法也是这里的分析重点。
系统的LayoutManager
的onLayoutChildren
方法是一个空方法,所以需要LayoutManager
的子类自己来实现。从这里,我们可以得出两个点。
- 子类
LayoutManager
需要自己实现onLayoutChildren
方法,从而来决定RecyclerView
在该LayoutManager
的策略下,应该怎么布局。从这里,我们看出来RecyclerView
的灵活性。 LayoutManager
类似于ViewGroup
,将onLayoutChildren
方法(ViewGroup
是onLayout
方法)公开出来,这种模式在Android中很常见的。
这里,我先不对onLayoutChildren
方法进行展开,待会会详细的分析。接下来,我们来分析第三种情况--没有开启自动测量。
(3)没有开启自动测量
先来看看这段代码:
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
processAdapterUpdatesAndSetAnimationFlags();
onExitLayoutOrScroll();
if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
} else {
// consume remaining updates to provide a consistent state with the layout pass.
mAdapterHelper.consumeUpdatesInOnePass();
mState.mInPreLayout = false;
}
mAdapterUpdateDuringMeasure = false;
stopInterceptRequestLayout(false);
} else if (mState.mRunPredictiveAnimations) {
// If mAdapterUpdateDuringMeasure is false and mRunPredictiveAnimations is true:
// this means there is already an onMeasure() call performed to handle the pending
// adapter change, two onMeasure() calls can happen if RV is a child of LinearLayout
// with layout_width=MATCH_PARENT. RV cannot call LM.onMeasure() second time
// because getViewForPosition() will crash when LM uses a child to measure.
setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
return;
}
if (mAdapter != null) {
mState.mItemCount = mAdapter.getItemCount();
} else {
mState.mItemCount = 0;
}
startInterceptRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
stopInterceptRequestLayout(false);
mState.mInPreLayout = false; // clear
上面的代码我将分为两步:
- 如果
mHasFixedSize
为true(也就是调用了setHasFixedSize
方法),将直接调用LayoutManager
的onMeasure
方法进行测量。 - 如果
mHasFixedSize
为false,同时此时如果有数据更新,先处理数据更新的事务,然后调用LayoutManager
的onMeasure
方法进行测量。
通过上面的描述,我们知道,如果未开启自动测量,那么肯定会调用LayoutManager
的onMeasure
方法来进行测量,这就是LayoutManager
的onMeasure
方法的作用。
3.layout
measure
过程分析的差不多了,接下来我们就该分析第二个过程--layout
。我们来看看onLayout
方法:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
onLayout
方法本身没有做多少的事情,重点还是在dispatchLayout
方法里面。
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
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()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
dispatchLayout
保证RecyclerView
必须经历三个过程--dispatchLayoutStep1
、dispatchLayoutStep2
、dispatchLayoutStep3
。
同时,在后面的文章中,你会看到dispatchLayout
方法其实还为RecyclerView
节省了很多步骤,也就是说,在RecyclerView
经历一次完整的dispatchLayout
之后,后续如果参数有所变化时,可能只会经历最后的1步或者2步。
对于dispatchLayoutStep1
和dispatchLayoutStep2
方法,我们前面已经讲解了,这里就不做过多的解释了。这里,我们就简单的看一下dispatchLayoutStep3
方法吧。
private void dispatchLayoutStep3() {
// ······
mState.mLayoutStep = State.STEP_START;
// ······
}
为什么这里只是简单看一下dispatchLayoutStep3
方法呢?因为这个方法主要是做Item的动画,也就是我们熟知的ItemAnimator
的执行,而本文不对动画进行展开,所以先省略动画部分。
在这里,我们需要关注dispatchLayoutStep3
方法的是,它将mLayoutStep
重置为了State.STEP_START
。也就是说如果下一次重新开始dispatchLayout
的话,那么肯定会经历dispatchLayoutStep1
、dispatchLayoutStep2
、dispatchLayoutStep3
三个方法。
以上就是RecyclerView
的layout过程,是不是感觉非常的简单?RecyclerView
跟其他ViewGroup
不同的地方在于,如果开启了自动测量,在measure
阶段,已经将Children
布局完成了;如果没有开启自动测量,则在layout
阶段才布局Children
。
4.draw
接下来,我们来分析三大流程的最后一个阶段--draw
。在正式分析draw过程之前,我先来对RecyclerView
的draw
做一个概述。RecyclerView
分为三步:
- 调用
super.draw
方法。这里主要做了两件事:1. 将Children
的绘制分发给ViewGroup
;2. 将分割线的绘制分发给ItemDecoration
。 - 如果需要的话,调用
ItemDecoration
的onDrawOver
方法。通过这个方法,我们在每个ItemView
上面画上很多东西。 - 如果
RecyclerView
调用了setClipToPadding,会实现一种特殊的滑动效果–每个ItemView可以滑动到padding区域。
public void draw(Canvas c) {
// 第一步
super.draw(c);
// 第二步
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
// 第三步
// TODO If padding is not 0 and clipChildrenToPadding is false, to draw glows properly, we
// need find children closest to edges. Not sure if it is worth the effort.
// ······
}
熟悉三大流程的同学,肯定知道第一步会回调到onDraw
方法里面,而ItemDecoration
的绘制就是在onDraw
方法里面。
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
onDraw
方法主要是将ItemDecoration
的绘制分发到ItemDecoration
的onDraw
方法里面去。从这里,我们可以看出来,RecyclerView
的设计实在是太灵活了!
5.LayoutManager的onLayoutChildren方法
从整体来说,RecyclerView
的三大流程还是比较简单,不过在整个过程中,我们似乎忽略了一个过程--那就是RecyclerView
到底是怎么layout children
的?
前面在介绍dispatchLayoutStep2
方法时,只是简单的介绍了,RecyclerView
通过调用LayoutManager
的onLayoutChildren
方法。LayoutManager
本身对这个方法没有进行实现,所以必须得看看它的子类,这里我们就来看看LinearLayoutManager
。
由于LinearLayoutManager
的onLayoutChildren
方法比较长,这里不可能贴出完整的代码,所以这里我先对这个方法做一个简单的概述,方便理解。
- 确定锚点的信息,这里面的信息包括:1.
Children
的布局方向,有start和end两个方向;2.mPosition
和mCoordinate
,分别表示Children
开始填充的position和坐标。 - 调用
detachAndScrapAttachedViews
方法,detach
掉或者remove
掉RecyclerView
的Children
。这一点本来不在本文的讲解范围内,但是为了后续对RecyclerView
的缓存机制有更好的了解,这里特别的提醒一下。 - 根据锚点信息,调用
fill
方法进行Children
的填充。这个过程中根据锚点信息的不同,可能会调用两次fill
方法。
接下来我们来看看代码:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// layout algorithm:
// 1) by checking children and other variables, find an anchor coordinate and an anchor
// item position.
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
// create layout state
// ······
// 第一步
final View focused = getFocusedChild();
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
}
// ······
// 第二步
detachAndScrapAttachedViews(recycler);
mLayoutState.mIsPreLayout = state.isPreLayout();
// 第三步
if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
final int firstElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForEnd += mLayoutState.mAvailable;
}
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
// end could not consume all. add more items towards start
extraForStart = mLayoutState.mAvailable;
updateLayoutStateToFillStart(firstElement, startOffset);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
// start could not consume all it should. add more items towards end
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
// ······
}
相信从上面的代码都可以找出每一步的执行。现在,我们来详细分析每一步。首先来看第一步--确定锚点的信息
。
要想看锚点信息的计算过程,我们可以从updateAnchorInfoForLayout
方法里面来找出答案,我们来看看updateAnchorInfoForLayout
方法:
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
AnchorInfo anchorInfo) {
// 第一种计算方式
if (updateAnchorFromPendingData(state, anchorInfo)) {
return;
}
// 第二种计算方式
if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
return;
}
// 第三种计算方式
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
简单分析一下这三种计算所做的含义:
- 第一种计算方式,表示含义有两种:1.
RecyclerView
被重建,期间回调了onSaveInstanceState
方法,所以目的是为了恢复上次的布局;2.RecyclerView
调用了scrollToPosition
之类的方法,所以目的是让RecyclerView
滚到准确的位置上去。所以,锚点的信息根据上面的两种情况来计算。 - 第二种计算方法,从
Children
上面来计算锚点信息。这种计算方式也有两种情况:1. 如果当前有拥有焦点的Child
,那么有当前有焦点的Child的位置来计算锚点;2. 如果没有child拥有焦点,那么根据布局方向(此时布局方向由mLayoutFromEnd
来决定)获取可见的第一个ItemView
或者最后一个ItemView
。 - 如果前面两种方式都计算失败了,那么采用第三种计算方式,也就是默认的计算方式。
以上就是updateAnchorInfoForLayout
方法所做的事情,这里就不详细纠结每种计算方式的细节。至于第二步,调用detachAndScrapAttachedViews
方法对所有的ItemView
进行回收,这部分的内容属于RecyclerView
缓存机制的部分。
接下来我们来看看第三步,也就是调用fill
方法来填充Children
。在正式分析填充过程时,我们先来看一张图片:
上图形象的展现出三种fill
的情况。其中,我们可以看到第三种情况,fill
方法被调用了两次。我们看看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);
}
// ······
}
fill
方法的代码比较长,其实都是来计算可填充的空间,真正填充Child
的地方是layoutChunk
方法。我们来看看layoutChunk
方法。
由于layoutChunk
方法比较长,这里我就不完整的展示,为了方便理解,我对这个方法做一个简单的概述,让大家有一个大概的理解。
- 调用
LayoutState
的next
方法获得一个ItemView
。千万别小看这个next
方法,RecyclerView
缓存机制的起点就是从这个方法开始,可想而知,这个方法到底为我们做了多少事情。 - 如果
RecyclerView
是第一次布局Children的话(layoutState.mScrapList == null
为true),会先调用addView,将View
添加到RecyclerView
里面去。 - 调用
measureChildWithMargins
方法,测量每个ItemView
的宽高。注意这个方法测量ItemView的宽高考虑到了两个因素:1.margin属性;2.ItemDecoration
的offset
。 - 调用
layoutDecoratedWithMargins
方法,布局ItemView
。这里也考虑上面的两个因素的。
至于每一步具体干了嘛,这里就不详细的解释,都是一些基本操作。综上所述,便是LayoutManager
的onLayoutChildren
方法整个执行过程。
6.总结
RecyclerView
的measure
过程分为三种情况,每种情况都有执行过程。通常来说,我们都会走自动测量的过程。- 自动测量里面需要分清楚
mState.mLayoutStep
状态值,因为根据不同的状态值调用不同的dispatchLayoutStep
方法。 layout
过程也根据mState.mLayoutStep
状态来调用不同的dispatchLayoutStep
方法draw
过程主要做了四件事:1.绘制ItemDecoration
的onDraw
部分;2.绘制Children
;3.绘制ItemDecoration
的drawOver
部分;4. 根据mClipToPadding
的值来判断是否进行特殊绘制。
二.Recycleview的滑动机制
从RecyclerView
的类结构上来看,我们知道RecyclerView
实现了NestedScrollingChild
接口,所以RecyclerView
也是一个可以产生滑动事件的View
。我相信大家都有用过CoordinatorLayout
和RecyclerView
这个组合,这其中原理的也是嵌套滑动。
本文打算从如下几个方面来分析RecyclerView
:
- 正常的
TouchEvent
- 嵌套滑动(穿插着文章各个地方,不会专门的讲解)
- 多指滑动
- fling滑动
1.传统事件
首先我们来看看onTouchEvent
方法:
@Override
public boolean onTouchEvent(MotionEvent e) {
// ······
if (dispatchOnItemTouch(e)) {
cancelTouch();
return true;
}
// ······
switch (action) {
case MotionEvent.ACTION_DOWN: {
// ······
} break;
case MotionEvent.ACTION_POINTER_DOWN: {
// ······
} break;
case MotionEvent.ACTION_MOVE: {
// ······
} break;
case MotionEvent.ACTION_POINTER_UP: {
// ······
} break;
case MotionEvent.ACTION_UP: {
// ······
} break;
case MotionEvent.ACTION_CANCEL: {
cancelTouch();
} break;
}
// ······
return true;
}
对ACTION_POINTER_DOWN
和ACTION_POINTER_UP
事件比较陌生,这两个事件就跟多指滑动有关,
在分析源码之前,我先将上面的代码做一个简单的概述。
- 如果当前的
mActiveOnItemTouchListener
需要消耗当前事件,那么优先交给它处理。 - 如果
mActiveOnItemTouchListener
不消耗当前事件,那么就走正常的事件分发机制。这里面有很多的细节,稍后我会详细的介绍。
关于第一步,这里不用我来解释,它就是一个Listener
的回调,非常的简单,我们重点在于分析第二步。
(1)down事件
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
这里主要做了两件事:
- 记录下Down事件的x、y坐标。
- 调用
startNestedScroll
方法,询问父View
是否处理事件。
(2)move事件
move事件的源码比较长,且不难读懂,源码就先不放此文了,直接说下move的步骤:
- 根据Move事件产生的x、y坐标来计算dx、dy。
- 调用
dispatchNestedPreScroll
询问父View
是否优先处理滑动事件,如果要消耗,dx和dy分别会减去父View
消耗的那部分距离。 - 然后根据情况来判断
RecyclerView
是垂直滑动还是水平滑动,最终是调用scrollByInternal
方法来实现滑动的效果的。 - 调用
GapWorker
的postFromTraversal
来预取ViewHolder
。这个过程会走缓存机制部分的逻辑,同时也有可能会调用Adapter
的onBindViewHolder
方法来提前加载数据。
其中第一步和第二步都是比较简单的,这里就直接省略。
而scrollByInternal
方法也是非常的简单,在scrollByInternal
方法内部,实际上是调用了LayoutManager
的scrollHorizontallyBy
方法或者scrollVerticallyBy
方法来实现的。LayoutManager
这两个方法实际上也没有做什么比较骚的操作,归根结底,最终调用了就是调用了每个Child
的offsetTopAndBottom
或者offsetLeftAndRight
方法来实现的,这里就不一一的跟踪代码了。
在这里,我们就简单分析下GapWorker是怎么进行领取的,看看postFromTraversal方法:
void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
if (recyclerView.isAttachedToWindow()) {
if (RecyclerView.DEBUG && !mRecyclerViews.contains(recyclerView)) {
throw new IllegalStateException("attempting to post unregistered view!");
}
if (mPostTimeNs == 0) {
mPostTimeNs = recyclerView.getNanoTime();
recyclerView.post(this);
}
}
recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
}
在postFromTraversal
方法内部也没有做多少事情,最核心在于调用了post
方法,向任务队列里面添加了一个Runnable
。看来重点的分析还是GapWorker
的run
方法:
@Override
public void run() {
try {
TraceCompat.beginSection(RecyclerView.TRACE_PREFETCH_TAG);
if (mRecyclerViews.isEmpty()) {
// abort - no work to do
return;
}
// Query most recent vsync so we can predict next one. Note that drawing time not yet
// valid in animation/input callbacks, so query it here to be safe.
final int size = mRecyclerViews.size();
long latestFrameVsyncMs = 0;
for (int i = 0; i < size; i++) {
RecyclerView view = mRecyclerViews.get(i);
if (view.getWindowVisibility() == View.VISIBLE) {
latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs);
}
}
if (latestFrameVsyncMs == 0) {
// abort - either no views visible, or couldn't get last vsync for estimating next
return;
}
long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;
prefetch(nextFrameNs);
// TODO: consider rescheduling self, if there's more work to do
} finally {
mPostTimeNs = 0;
TraceCompat.endSection();
}
}
run
方法的逻辑也是非常简单,首先计算获得下一帧的时间,然后调用prefetch
方法进行预取ViewHolder
。
void prefetch(long deadlineNs) {
buildTaskList();
flushTasksWithDeadline(deadlineNs);
}
prefetch
方法也简单,显示调用buildTaskList
方法生成任务队列,然后调用flushTasksWithDeadline
来执行task
,这其中会调用RecyclerView
的tryGetViewHolderForPositionByDeadline
方法来获取一个ViewHolder
,这里就不一一分析了。
不过需要提一句的是,tryGetViewHolderForPositionByDeadline
方法是整个RecyclerView
缓存机制的核心,RecyclerView
缓存机制在这个方法被淋漓尽致的体现出来。最后就是Up事件和Cancel事件,这两个事件更加的简单,都进行一些清理的操作,这里就不分析了。不过在Up事件里面,有一个特殊事件可能会产生–fling事件,待会我们会详细的分析。
2.多指滑动
多指滑动的意思不是指RecyclerView
能够相应多根手指的滑动,而是指当一个手指还没释放时,此时另一个手指按下,此时RecyclerView
就不相应上一个手指的手势,而是相应最近按下手指的手势。
case MotionEvent.ACTION_POINTER_DOWN: {
mScrollPointerId = e.getPointerId(actionIndex);
mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
} break;
当另一个手指按下时,此时就会立即更新按下的坐标,同时会更新mScrollPointerId
,表示后面只会响应最近按下手指的手势。
其次,我们来看看多指松开的情况:
case MotionEvent.ACTION_POINTER_UP: {
onPointerUp(e);
} break;
private void onPointerUp(MotionEvent e) {
final int actionIndex = e.getActionIndex();
if (e.getPointerId(actionIndex) == mScrollPointerId) {
// Pick a new pointer to pick up the slack.
final int newIndex = actionIndex == 0 ? 1 : 0;
mScrollPointerId = e.getPointerId(newIndex);
mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
}
}
在这里没有什么特别的操作,就是普通的更新。
3.fling滑动
我们先来看看fling
滑动产生的地方,也是Up事件的地方:
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally
? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
} break;
从上面的代码中,我们可以看出来,最终是调用fling
方法来是实现fling
效果的,我们来看看fling
方法:
public boolean fling(int velocityX, int velocityY) {
// ······
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);
if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
return true;
}
if (canScroll) {
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontal) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertical) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}
在fling
方法里面,显示调用dispatchNestedPreFling
方法询问父View
是否处理fling
事件,最后调用ViewFlinger
的fling
方法来实现fling
效果,所以真正的核心在于ViewFlinger
的fling
方法里面,我们继续来看:
public void fling(int velocityX, int velocityY) {
setScrollState(SCROLL_STATE_SETTLING);
mLastFlingX = mLastFlingY = 0;
mScroller.fling(0, 0, velocityX, velocityY,
Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
postOnAnimation();
}
在ViewFlinger
的fling
方法里面,先是调用了OverScroller
的fling
来计算fling
相关的参数,包括fling
的距离和fling
的时间。这里就不深入的分析计算相关的代码,因为这里面都是一些数学和物理的计算。最后就是调用了postOnAnimation
方法。
void postOnAnimation() {
if (mEatRunOnAnimationRequest) {
mReSchedulePostAnimationCallback = true;
} else {
removeCallbacks(this);
ViewCompat.postOnAnimation(RecyclerView.this, this);
}
}
其实跟View
的post
差不多,所以最终还是得看ViewFlinger
的run
方法。ViewFlinger
的run
方法比较长,这里我将它简化了一下:
public void run() {
// ······
// 第一步,更新滚动信息,并且判断当前是否已经滚动完毕
// 为true表示未滚动完毕
if (scroller.computeScrollOffset()) {
//······
if (mAdapter != null) {
// ······
// 滚动特定距离
if (dx != 0) {
hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
overscrollX = dx - hresult;
}
if (dy != 0) {
vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
overscrollY = dy - vresult;
}
// ······
}
// ······
// 如果滚动完毕,就是调用finish方法;
// 如果没有滚动完毕,就调用postOnAnimation方法继续递归
if (scroller.isFinished() || (!fullyConsumedAny
&& !hasNestedScrollingParent(TYPE_NON_TOUCH))) {
// setting state to idle will stop this.
setScrollState(SCROLL_STATE_IDLE);
if (ALLOW_THREAD_GAP_WORK) {
mPrefetchRegistry.clearPrefetchPositions();
}
stopNestedScroll(TYPE_NON_TOUCH);
} else {
postOnAnimation();
if (mGapWorker != null) {
mGapWorker.postFromTraversal(RecyclerView.this, dx, dy);
}
}
}
// ······
}
整个fling
核心就在这里,通过上面的三步,最终就是实现了fling的效果,上面的注意已经非常的清晰了,这里就不继续分析了。
三.recycleview的缓存机制
1.概述
(1).四级缓存
首先,我将RecyclerView
的缓存分为四级,可能有的人将它分为三级,这些看个人的理解。这里统一说明一下每级缓存的意思。
缓存级别 | 实际变量 | 含义 |
---|---|---|
一级缓存 | mAttachedScrap 和mChangedScrap | 这是优先级最高的缓存, |
二级缓存 | mCachedViews | 默认大小为2,通常用来存储预取的ViewHolder ,同时在回收ViewHolder 时,也会可能存储一部分的ViewHolder ,这部分的ViewHolder 通常来说,意义跟一级缓存差不多。 |
三级缓存 | ViewCacheExtension | 自定义缓存,通常用不到 |
四级缓存 | RecyclerViewPool | 根据ViewType 来缓存ViewHolder ,每个ViewType 的数组大小为5,可以动态的改变。 |
如上表,统一的解释了每个缓存的含义和作用。在这里,我再来对其中的几个缓存做一个详细的解释。
mAttachedScrap
:上表中说,它表示存储的是当前还在屏幕中ViewHolder
。实际上是从屏幕上分离出来的ViewHolder
,但是又即将添加到屏幕上去的ViewHolder
。比如说,RecyclerView
上下滑动,滑出一个新的Item
,此时会重新调用LayoutManager
的onLayoutChildren
方法,从而会将屏幕上所有的ViewHolder
先scrap
掉(含义就是废弃掉),添加到mAttachedScrap
里面去,然后在重新布局每个ItemView
时,会从优先mAttachedScrap
里面获取,这样效率就会非常的高。这个过程不会重新onBindViewHolder
。-
mCachedViews
:默认大小为2,不过通常是3,3由默认的大小2 + 预取的个数1。所以在RecyclerView
在首次加载时,mCachedViews
的size
为3(这里以LinearLayoutManager
的垂直布局为例)。通常来说,可以通过RecyclerView
的setItemViewCacheSize
方法设置大小,但是这个不包括预取大小;预取大小通过LayoutManager
的setItemPrefetchEnabled
方法来控制。
(2).ViewHolder的几个状态值
我们在看RecyclerView
的源码时,可能到处都能看到调用ViewHolder
的isInvalid
、isRemoved
、isBound
、isTmpDetached
、isScrap
和isUpdated
这几个方法。
方法名 | 对应的flag | 含义或者状态设置的时机 |
---|---|---|
isInvalid | FLAG_INVALID | 表示当前 |
isRemoved | FLAG_REMOVED | 表示当前的ViewHolder 是否被移除。通常来说,数据源被移除了部分数据,然后调用Adapter 的notifyItemRemoved 方法。 |
isBound | FLAG_BOUND | 表示当前ViewHolder 是否已经调用了onBindViewHolder 。 |
isTmpDetached | FLAG_TMP_DETACHED | 表示当前的 |
isScrap | 无Flag来表示该状态,用mScrapContainer 是否为null来判断 | 表示是否在mAttachedScrap 或者mChangedScrap 数组里面,进而表示当前ViewHolder 是否被废弃。 |
isUpdated | FLAG_UPDATE | 表示当前 |
(3).ChildHelper的mHiddenViews
在四级缓存中,我们并没有将mHiddenViews
算入其中。因为mHiddenViews
只在动画期间才会有元素,当动画结束了,自然就清空了。所以mHiddenViews
并不算入4级缓存中。
这里还有一个问题,就是上面在解释mChangedScrap
时,也在说,当调用Adapter
的notifyItemChanged
方法,会将更新了的ViewHolder
反放入mChangedScrap
数组里面。那到底是放入mChangedScrap
还是mHiddenViews
呢?同时可能有人对mChangedScrap
和mAttachedScrap
有疑问,这里我做一个统一的解释:
首先,如果调用了Adapter
的notifyItemChanged
方法,会重新回调到LayoutManager
的onLayoutChildren
方法里面,而在onLayoutChildren
方法里面,会将屏幕上所有的ViewHolder
回收到mAttachedScrap
和mChangedScrap
。这个过程就是将ViewHolder
分别放到mAttachedScrap
和mChangedScrap
,而什么条件下放在mAttachedScrap
,什么条件放在mChangedScrap
,这个就是他们俩的区别。
接下来我们来看一段代码,就能分清mAttachedScrap
和mChangedScrap
的区别了:
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
throw new IllegalArgumentException("Called scrap view with an invalid view."
+ " Invalid views cannot be reused from scrap, they should rebound from"
+ " recycler pool." + exceptionLabel());
}
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
可能很多人初次看到这方法时,会非常的懵逼,我也是如此。今天我们就来看看这个方法。这个根本的目的就是,判断ViewHolder
的flag状态,从而来决定是放入mAttachedScrap
还是mChangedScrap
。从上面的代码,我们得出:
mAttachedScrap
里面放的是两种状态的ViewHolder
:1.被同时标记为remove
和invalid
;2.完全没有改变的ViewHolder
。这里还有第三个判断,这个跟RecyclerView
的ItemAnimator
有关,如果ItemAnimator
为空或者ItemAnimator
的canReuseUpdatedViewHolder
方法为true,也会放入到mAttachedScrap
。那正常情况下,什么情况返回为true呢?从SimpleItemAnimator
的源码可以看出来,当ViewHolder
的isInvalid
方法返回为true时,会放入到mAttachedScrap
里面。也就是说,如果ViewHolder
失效了,也会放到mAttachedScrap
里面。-
那么
mChangedScrap
里面放什么类型flag的ViewHolder
呢?当然是ViewHolder
的isUpdated
方法返回为true时,会放入到mChangedScrap
里面去。所以,调用Adapter
的notifyItemChanged
方法时,并且RecyclerView
的ItemAnimator
不为空,会放入到mChangedScrap
里面。
了解了mAttachedScrap
和mChangedScrap
的区别之后,接下我们来看Scrap
数组和mHiddenViews
的区别。
mHiddenViews
只存放动画的ViewHolder
,动画结束了自然就清空了。之所以存在 mHiddenViews
这个数组,我猜测是存在动画期间,进行复用的可能性,此时就可以在mHiddenViews
进行复用了。而Scrap
数组跟mHiddenViews
两者完全不冲突,所以存在一个ViewHolder
同时在Scrap
数组和mHiddenViews
的可能性。但是这并不影响,因为在动画结束时,会从mHiddenViews
里面移除。
2.复用
RecyclerView
对ViewHolder
的复用,我们得从LayoutState
的next
方法开始。LayoutManager
在布局itemView
时,需要获取一个ViewHolder
对象,就是通过这个方法来获取,具体的复用逻辑也是在这个方面开始调用的。我们来看看:
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
next
方法里面其实也没做什么事,就是调用RecyclerView
的getViewForPosition
方法来获取一个View
的。而getViewForPosition
方法最终会调用到RecyclerView
的tryGetViewHolderForPositionByDeadline
方法。所以,RecyclerView
真正复用的核心就在这个方法,我们今天来详细的分析一下这个方法。
(1).通过position方式来获取ViewHolder
通过这种方式来获取优先级比较高,因为每个ViewHolder
还没被改变,通常在这种情况下,都是某一个ItemView
对应的ViewHolder
被更新导致的,所以在屏幕上其他的ViewHolder
,可以快速对应原来的ItemView
。我们来看看相关的源码。
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
如上的代码分为两步:
- 从
mChangedScrap
里面去获取ViewHolder
,这里面存储的是更新的ViewHolder
。 - 分别
mAttachedScrap
、mHiddenViews
、mCachedViews
获取ViewHolder。
我们来简单的分析一下这两步。先来看看第一步。
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
如果当前是预布局阶段,那么就从mChangedScrap
里面去获取ViewHolder
。那什么阶段是预布局阶段呢?这里我对预布局这个概念简单的解释。
预布局又可以称之为preLayout
,当当前的RecyclerView
处于dispatchLayoutStep1
阶段时,称之为预布局;dispatchLayoutStep2
称为真正布局的阶段;dispatchLayoutStep3
称为postLayout
阶段。同时要想真正开启预布局,必须有ItemAnimator
,并且每个RecyclerView
对应的LayoutManager
必须开启预处理动画。
在这里,为了简单,只要RecyclerView
处于dispatchLayoutStep1
,我们就当做它处于预布局阶段。
为什么只在预布局的时候才从mChangedScrap
里面去取呢?
首先,我们得知道mChangedScrap
数组里面放的是什么类型的 ViewHolder
。从前面的分析中,我们知道,只有当ItemAnimator
不为空,被changed的ViewHolder
会放在mChangedScrap
数组里面。因为chang动画前后相同位置上的ViewHolder
是不同的,所以当预布局时,从mChangedScrap
缓存里面取,而正式布局时,不会从mChangedScrap
缓存里面去,这就保证了动画前后相同位置上是不同的ViewHolder
。为什么要保证动画前后是不同的ViewHolder
呢?这是RecyclerView
动画机制相关的知识,这里就不详细的解释,后续有专门的文章来分析它,在这里,我们只需要记住,chang动画执行的有一个前提就是动画前后是不同的ViewHolder
。
然后,我们再来看看第二步。
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
这一步理解起来比较容易,分别从mAttachedScrap
、 mHiddenViews
、mCachedViews
获取ViewHolder
。但是我们需要的是,如果获取的ViewHolder
是无效的,得做一些清理操作,然后重新放入到缓存里面,具体对应的缓存就是mCacheViews
和RecyclerViewPool
。recycleViewHolderInternal
方法就是回收ViewHolder
的方法,后面再分析回收相关的逻辑会重点分析这个方法,这里就不进行追究了。
(2).通过viewType方式来获取ViewHolder
前面分析了通过Position的方式来获取ViewHolder
,这里我们来分析一下第二种方式--ViewType
。不过在这里,我先对前面的方式做一个简单的总结,RecyclerView
通过Position
来获取ViewHolder
,并不需要判断ViewType
是否合法,因为如果能够通过Position
来获取ViewHolder
,ViewType
本身就是正确对应的。
而这里通过ViewType
来获取ViewHolder
表示,此时ViewHolder
缓存的Position
已经失效了。ViewType
方式来获取ViewHolder
的过程,我将它分为3步:
- 如果
Adapter
的hasStableIds
方法返回为true,优先通过ViewType
和id
两个条件来寻找。如果没有找到,那么就进行第2步。 - 如果
Adapter
的hasStableIds
方法返回为false,在这种情况下,首先会在ViewCacheExtension
里面找,如果还没有找到的话,最后会在RecyclerViewPool
里面来获取ViewHolder。 - 如果以上的复用步骤都没有找到合适的
ViewHolder
,最后就会调用Adapter
的onCreateViewHolder
方法来创建一个新的ViewHolder
。
在这里,我们需要注意的是,上面的第1步 和 第2步有前提条件,就是两个都必须比较ViewType
。接下来,我通过代码简单的分析一下每一步。
A.通过id寻找合适的ViewHolder
通过id寻找合适的ViewHolder
主要是通过调用getScrapOrCachedViewForId
方法来实现的,我们简单的看一下代码:
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
而getScrapOrCachedViewForId
方法本身没有什么分析的必要,就是分别从mAttachedScrap
和mCachedViews
数组寻找合适的ViewHolder
。
B.从RecycleViewPool里面获取ViewHolder
ViewCacheExtension
存在的情况是非常的少见,这里为了简单,就不展开了(实际上我也不懂!),所以这里,我们直接来看RecyclerViewPool
方式。
在这里,我们需要了解RecyclerViewPool
的数组结构。我们简单的分析一下RecyclerViewPool
这个类。
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
在RecyclerViewPool
的内部,使用SparseArray
来存储每个ViewType
对应的ViewHolder
数组,其中每个数组的最大size为5。这个数据结构是不是非常简单呢?
简单的了解了RecyclerViewPool
的数据结构,接下来我们来看看复用的相关的代码:
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
C.调用Adapter的onCreateViewHolder方法创建一个新的ViewHolder
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
holder = mAdapter.createViewHolder(RecyclerView.this, type);
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
RecyclerView innerView = findNestedRecyclerView(holder.itemView);
if (innerView != null) {
holder.mNestedRecyclerView = new WeakReference<>(innerView);
}
}
long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
}
}
上面的代码主要的目的就是调用Adapter
的createViewHolder
方法来创建一个ViewHolder
,在这个过程就是简单计算了创建一个ViewHolder
的时间。
3.回收
回收是RecyclerView
复用机制内部非常重要。首先,有复用的过程,肯定就有回收的过程;其次,同时理解了复用和回收两个过程,这可以帮助我们在宏观上理解RecyclerView
的工作原理;最后,理解RecyclerView
在何时会回收ViewHolder
,这对使用RecyclerView
有很大的帮助。
其实回收的机制也没有想象中那么的难,本文打算从几个方面来分析RecyclerView
的回收过程。
- scrap数组
- mCacheViews数组
- mHiddenViews数组
- RecyclerViewPool数组
(1).scrap数组
关于ViewHolder
回收到scrap
数组里面,其实我在前面已经简单的分析了,重点就在于Recycler
的scrapView
方法里面。我们来看看scrapView
在哪里被调用了。有如下两个地方:
- 在
getScrapOrHiddenOrCachedHolderForPosition
方法里面,如果从mHiddenViews
获得一个ViewHolder
的话,会先将这个ViewHolder
从mHiddenViews
数组里面移除,然后调用Recycler
的scrapView
方法将这个ViewHolder
放入到scrap
数组里面,并且标记FLAG_RETURNED_FROM_SCRAP
和FLAG_BOUNCED_FROM_HIDDEN_LIST
两个flag。 - 在
LayoutManager
里面的scrapOrRecycleView
方法也会调用Recycler
的scrapView
方法。而有两种情形下会出现如此情况:1. 手动调用了LayoutManager
相关的方法;2.RecyclerView
进行了一次布局(调用了requestLayout
方法)
(2).mCacheViews数组
mCacheViews
数组作为二级缓存,回收的路径相较于一级缓存要多。关于mCacheViews数组,重点在于Recycler
的recycleViewHolderInternal
方法里面。我将mCacheViews
数组的回收路径大概分为三类,我们来看看:
- 在重新布局回收了。这种情况主要出现在调用了
Adapter
的notifyDataSetChange
方法,并且此时Adapter
的hasStableIds
方法返回为false。从这里看出来,为什么notifyDataSetChange
方法效率为什么那么低,同时也知道了为什么重写hasStableIds
方法可以提高效率。因为notifyDataSetChange
方法使得RecyclerView
将回收的ViewHolder
放在二级缓存,效率自然比较低。 - 在复用时,从一级缓存里面获取到
ViewHolder
,但是此时这个ViewHolder
已经不符合一级缓存的特点了(比如Position失效了,跟ViewType对不齐),就会从一级缓存里面移除这个ViewHolder
,从添加到mCacheViews
里面 - 当调用
removeAnimatingView
方法时,如果当前ViewHolder
被标记为remove,会调用recycleViewHolderInternal
方法来回收对应的ViewHolder
。调用removeAnimatingView
方法的时机表示当前的ItemAnimator
已经做完了
(3).mHiddenViews数组
一个ViewHolder
回收到mHiddenView
数组里面的条件比较简单,如果当前操作支持动画,就会调用到RecyclerView
的addAnimatingView
方法,在这个方法里面会将做动画的那个View
添加到mHiddenView
数组里面去。通常就是动画期间可以会进行复用,因为mHiddenViews
只在动画期间才会有元素。
(4).RecyclerViewPool
RecyclerViewPool
跟mCacheViews
,都是通过recycleViewHolderInternal
方法来进行回收,所以情景与mCacheViews
差不多,只不过当不满足放入mCacheViews
时,才会放入到RecyclerViewPool
里面去。
(5).为什么hasStableIds方法返回true会提高效率呢?
了解了RecyclerView
的复用和回收机制之后,这个问题就变得很简单了。我从两个方面来解释原因。
A.复用方面
我们先来看看复用怎么能体现hasStableIds
能提高效率呢?来看看代码:
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
在前面通过Position
方式来获取一个ViewHolder
失败之后,如果Adapter
的hasStableIds
方法返回为true,在进行通过ViewType
方式来获取ViewHolder
时,会优先到1级或者二级缓存里面去寻找,而不是直接去RecyclerViewPool
里面去寻找。从这里,我们可以看到,在复用方面,hasStableIds
方法提高了效率。
B.回收方面
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
if (DEBUG) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
从上面的代码中,我们可以看出,如果hasStableIds
方法返回为true的话,这里所有的回收都进入scrap
数组里面。这刚好与前面对应了。
通过如上两点,我们就能很好的理解为什么hasStableIds
方法返回true会提高效率。
4.总结
RecyclerView
回收和复用机制到这里分析的差不多了。这里做一个小小的总结。
- 在
RecyclerView
内部有4级缓存,每一级的缓存所代表的意思都不一样,同时复用的优先也是从上到下,各自的回收也是不一样。 mHideenViews
的存在是为了解决在动画期间进行复用的问题。ViewHolder
内部有很多的flag,在理解回收和复用机制之前,最好是将ViewHolder
的flag梳理清楚。
最后用一张图片来结束本章节的介绍。