RecyclerView缓存机制和刷新原理

最近刚好在看RecyclerView源码方面知识,包括缓存机制和刷新原理,所以打算写下来,就当复习整理了。
使用RecyclerView时会使用adapter和viewHolder来实现,且viewHolder实际上是跟itemView一一对应的,所以当我们说缓存view时,其实是缓存viewHolder。

一.缓存机制
RecyclerView有四个缓存机制:

1.scrap:缓存当前屏幕内的viewHolder。
scrap层的缓存在代码中的实现是用mAttachedScrap和mChangedScrap来实现的。
mAttachedScrap:缓存屏幕内的viewHolder。从RecyclerView的onLayout开始,调用了onLayoutChildren把所有的view都存在这里。通过item的position或者itemid找到对应的viewHolder,一般情况下,viewHolder并不会走onBindViewHolder方法。
mChangedScrap:同样也是缓存屏幕内的viewHolder。从名字可以看出这是缓存了有变化的viewHolder。使用场景是当调用了notifyItemChanged等时才会使用mChangedScrap来缓存viewHolder。但是只有在预布局的时候才会用到,真正布局时只会创建新的viewHolder。

2.cache:使用mCachedViews来缓存滑动时即将与RecyclerView分离的ViewHolder,按子View的position或id缓存。
出现场景是滑动RecyclerView或者调用notify。默认最多存放2个,开发者可以调用setItemViewCacheSize(int size)来增大缓存量。从源码可以发现当超出了限制后,会根据FIFO原则把viewHolder拿出来并存放到下一级缓存中(剧透一下:RecyclerViewPool)

3.ViewCacheExtension:这是需要开发者去自定义的缓存,一般来说页面上的所有数据都可以缓存到这里。
使用场景待会再讲。虽然自定义缓存有一定的自由度,但是也是因为完全由开发者自己实现缓存,也就会有一定的问题,所以慎用!

4.RecycledViewPool:ViewHolder缓存池。
如上所说,在有限的mCachedViews中如果存不下ViewHolder时,就会把ViewHolder存入mRecyclerViewPool中。从这里拿出来的viewHolder是根据viewType而不是position或itemid,而且会重置清空viewHolder。

以上就是RecyclerView的四级缓存机制。它们之间的关系如下图

RecyclerView 四级缓存关系
RecyclerView 四级缓存关系

让我们再看一下第三级缓存ViewCacheExtension。为什么需要这个呢?
可以假设这样一个场景:当我们有一个itemView(或者说viewHolder)它被移出了屏幕外,这时进入了mCacheViews里面,如果超出了限制又会进入到mRecyclerViewPool里面。根据前面我们得知从mRecyclerViewPool里面出来的viewHolder数据会被清空,那么势必会重新绑定adapter。如果只是普通的itemView并且数据会一直变化的话,那么有没有这个自定义无所谓。但是如果这个特定的itemView比如说position为0的,我们希望它一直就是保持position为0,并且它的数据不会改变,那么我们就可以使用ViewCacheExtension来缓存它,使它不会进入mRecyclerViewPool里面,就可以节省内存提高效率了。

1
2
3
4
5
6
7
8
9
10
class MyViewCacheExtension : RecyclerView.ViewCacheExtension() {
private val mAdapter: RecyclerView.Adapter<*>? = null
override fun getViewForPositionAndType(
recycler: RecyclerView.Recycler,
position: Int,
type: Int
): View? {
return if(type == TYPE_SPECIAL) mAdapter.data(position) else null
}
}

在activity调用mRecyclerView.setViewCacheExtension(mExtensionCache); 就行了

网上有个具体使用例子:https://github.com/shineM/RecyclerViewCacheDemo


二、大致的源码解析

1.初始化调用
View的绘制流程是从onMeasure开始的,所以先从RecyclerView的onMeasure入手。比较核心的是onMeasure里面的dispatchLayoutStep2,里面通过mLayout(即LayoutManager).onLayoutChildren,以LinearLayoutManager为例来分析:

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
@Override
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.-- 通过确认子view和其他变量来找到锚点坐标和位置
// 2) fill towards start, stacking from bottom --从底部向
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
// -- 实现滑动需求,如从底部开始绘制
……

detachAndScrapAttachedViews(recycler);

if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
fill(recycler, mLayoutState, state, false);

// fill towards end
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
} else {
// fill towards end
fill(recycler, mLayoutState, state, false);

// fill towards start
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
}

进入detachAndScrapAttachedViews->scrapOrRecycleView:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
……
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
……
} else {
// 因为是从初始化开始,先看这里
// detachViewAt会暂时移除view
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}

然后进入recycler.scrapView(view):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
// 可以看到在这里调用了mAttchedScrap把holder添加进去
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}

再回到scrapOrRecycleView里面,看看if的情况:

1
2
3
4
5
6
7
if(viewHolder.isInvalid()&&!viewHolder.isRemoved()
&&!mRecyclerView.mAdapter.hasStableIds()) {
// 移除这个view
removeViewAt(index);
// 使用回收机制
recycler.recycleViewHolderInternal(viewHolder);
}

来到了recycleViewHolderInternal:

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
void recycleViewHolderInternal(ViewHolder holder) {
……
boolean cached = false;
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
// 看这里。如果mCachedViews的容量超过后
// 就会使用recycleCachedViewAt处理第一个View
recycleCachedViewAt(0);
cachedViewSize--;
}

……
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
//在没有添加进mCachedViews的情况下,就直接放进RecyclerdViewPool中
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
……
// even if the holder is not removed, we still call this method so that it is removed
// from view holder lists.
mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}
void recycleCachedViewAt(int cachedViewIndex) {

ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
// 看到这里也是调用了addViewHolderToRecycledViewPool,
// 再一次说明了如果超过mCachedViews的容量就会把先进去的View放到RecyclerdViewPool
addViewHolderToRecycledViewPool(viewHolder, true);
mCachedViews.remove(cachedViewIndex);
}

void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
View itemView = holder.itemView;
······
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);//将holder添加到RecycledViewPool中
}

现在回到LinearLayout的onLayoutChildren,继续往下看,看到一直调用fill->layoutChunk,通过layoutState.next来获取view

1
2
3
4
5
6
7
8
9
10
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
//mScrapList是mAttachedScrap,但是它是只读的,而且只有在开启预测动画时才会被赋值,可先忽略
return nextViewFromScrapList();
}
//重点在这里
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}

getViewForPosition->tryGetViewHolderForPositionByDeadline

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
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
// 0) If there is a changed scrap, try to find from there
// 判断是否是是预布局(后面讲)
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
// 找不到,就从mAttachedViews和mCachedViews中找
// 正常来说是从这里开始的
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
……
}
if (holder == null && mViewCacheExtension != null) {
// 找不到的话,就从自定义的ViewCacheExtension中找
// 前提是要自定义,不然就会略过
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
……
}
if (holder == null) { // fallback to pool
// 从RecyclerdViewPool中找
// 插播一下,RecycledViewPool是数据结构长这样
/*
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData {
final ArrayList<RecyclerView.ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}

key是itemType,value就是一个list,容量最大为5
SparseArray<RecyclerView.RecycledViewPool.ScrapData> mScrap = new SparseArray<>();

holder从RecycledViewPool的获取
holder = getRecycledViewPool().getRecycledView(type);
*/
……
}
if (holder == null) {
//如果所有缓存中都找不到
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
if (mState.isPreLayout() && holder.isBound()) {
// 预布局状态不用更新什么
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
// 如果holder是需要更新或者无效等等
// 需要重新绑定
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
}

至此,大概的流程就这样结束了。很明显当第一次初始化时还没有任何缓存的viewHolder,所以会一直调用createViewHolder去创建。由于在onLayout的时候也会调用onLayoutChildren,所以初始化之后需要缓存,避免重复初始化

PS:从Scrap中取视图的同时就被移出了缓存。在onLayout这里最终会调用到dispatchLayoutStep3方法,如果Scrap还有缓存,那么缓存会被清空,清空的缓存会被添加到mCachedViews或者RecyclerPool中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void dispatchLayoutStep3() {
mLayout.removeAndRecycleScrapInt(mRecycler);
if (mRecycler.mChangedScrap != null) {
mRecycler.mChangedScrap.clear();
}
}

void removeAndRecycleScrapInt(RecyclerView.Recycler recycler) {
recycler.clearScrap();
}

void clearScrap() {
mAttachedScrap.clear();
if (mChangedScrap != null) {
mChangedScrap.clear();
}
}

2.滑动时的调用
从LinearLayout的scrollBy入手分析,最后又会调用fill

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
//从这里开始回收
recycleByLayoutState(recycler, layoutState);
}
……
}

以recycleViewsFromEnd为例,->recycleChildren->removeAndRecycleViewAt->recycler.recycleView->recycleViewHolderInternal,这个方法之前有分析,就是把viewHolder缓存到mCachedViews或mRecycledViewPool里面,这也说明了移除屏幕外的viewHolder就会存到里面。
当然回收后还会继续走layoutChunk(),复用或重新建立viewHolder。

三、既然都看了源码了,不妨再分析一个东西:预布局pre-layout

1.什么叫做预布局?为什么要用到这个呢?
当你现在的列表有{1,2,3},此时屏幕只能显示{1,2}。当你想要删除掉2时,调用notifyItemRemove时,能够产生把3上移到2这样的动画效果,是通过预布局来实现的。
预布局起到什么作用呢?首先,RecyclerView先通过预布局(preLayout)生成快照{1,2,3}(即使此时屏幕不可见,也要把3的“画”出来,然后删掉2后的结果快照(postLayout)就是{1,3}。把这两张快照做对比,得到3之前后位置点信息,就可以做这样的动画了。

那怎么实现呢?看源码
让我们再次愉快地从onMeasure->dispatchLayoutStep1,发现有一句:mState.mInPreLayout = mState.mRunPredictiveAnimations; 假设它为true(我们现在仅分析它是true的作用),既然mState.mInPreLayout为true了,那么mState.mRunPredictiveAnimations肯定也是true
继续看dispatchLayoutStep1的后续代码

1
2
3
4
5
6
7
8
9
10
11
12
    if(mState.mRunPredictiveAnimations) {
// Step 1: run prelayout: This will use the old positions of items. The layout manager
// is expected to layout everything, even removed items (though not to add removed
// items back to the container). This gives the pre-layout position of APPEARING views
// which come into existence as part of the real layout.
// Save old positions so that LayoutManager can run its mapping logic.
saveOldPositions();
……
// temporarily disable flag because we are asking for previous layout
mLayout.onLayoutChildren(mRecycler, mState);
……
}

这部分代码的大概内容是在删除item时,会先保存未删除前的布局和位置信息。
然后跟之前分析的onLayoutChildren一样,最后会调用fill来填充布局

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
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
……
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
……
/**
* Consume the available space if:
* * layoutChunk did not request to be ignored
* * OR we are laying out scrap children
* * OR we are not doing pre-layout
*/
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
……
}
……
}

补充:
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}

通过以上代码可以大概得知,当remainingSpace>0时调用layoutChunk来画item,可以理解为循环几次就画几个item。

此时再把目光放到if那里,如果是非预布局的情况下(即删掉后的布局postLayout),不管其他条件是否满足,remainingSpace会一直减掉使用的空间。以之前的例子,删掉2后只需要画{1,3},所以循环了两次后remainingSpace就会小于0
当在预布局的情况下呢,那么要进入if条件就必须满足layoutChunkResult.mIgnoreConsumed为false或者layoutState.mScrapList != null
先看layoutState.mScrapList,通过查看代码默认初始化是空的,在完成fill之后才会对mScrapList赋值一个list,然后再做一遍fill后又清空了。所以在预布局调用fill时,mScrapList一定是空的
那么如果要满足条件就只能是layoutChunkResult.mIgnoreConsumed为false
看了上面的代码,发现当params是isItemRemoved或isItemChanged时,mIgnoreConsumed才会为true。当我们调用notifyItemRemoved时,最终会把layoutParam.isItemRemoved返回true,这里就不具体分析。因此在绘制item2时,由于被标记了removed,所以mIgnoreConsumed为true,remainingSpace就不会减少,因此就可以多画一个item3。

总结:在preLayout时,由于被删除项所占的位置会被忽略,所以可以多画几个item

前面分析,在postLayout时只会画两个item,那么是怎么保证画的就是{1,3}而不是{1,2}呢?
在调用notifyItemRemoved,会添加一个FLAG_REMOVED。RecyclerView的缓存机制会把当前屏幕的item都保存到mAttachedScrap里面,然后最终在layoutChunk的拿holder时调用getScrapOrHiddenOrCachedHolderForPosition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();

// Try first for an exact, non-invalid match from scrap.
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
// if条件最后一个是holder.isRemoved必须为false
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
}

因此被标记为removed的item2就不会被拿出来了

通过我们前面看到的源码,在dispatchLayoutStep2时也会再次调用mLayout.onLayoutChildren

1
2
3
4
5
private void dispatchLayoutStep2() {
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
}

所以实际上是布局了两次,step1是旧布局,step2是真正的新布局

2.数据更新:
在更新RecyclerView的数据时,我们一般会使用notifyDataSetChanged 或 notifyItemChanged(position),这两个有什么区别呢?
notifyDataSetChanged->mObservable.notifyChanged()->onChanged->processDataSetCompletelyChanged->markKnownViewsInvalid

1
2
3
4
if(holder !=null&&!holder.shouldIgnore()){
// 可以看到在这里会给holder打上这样的标签
holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
}

最终调用requestLayout

补充知识:requestLayout会引发view的重绘,会调用View的onMeasure和onLayout,从前面也得到最后其实还是走了onLayoutChildren这些


再次看一下 recycleViewHolderInternal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void recycleViewHolderInternal(ViewHolder holder) {
……
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
……
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
……
}

中间省略了一些细节,可以大概看出如果holder在没有那几个特定的flag(包括FLAG_UPDATE和FLAG_INVALID)时,holder是会存到mCachedViews里面,如果有那些flag则会直接进入到RecycledViewPool里面。但是!RecycledViewPool是有一定的容量限制,如果数量过大则会重新创建ViewHolder

那么notifyItemChanged呢?
notifyItemChanged->mObservable.notifyItemRangeChanged->notifyItemRangeChanged

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
assertNotInLayoutOrScroll(null);
//只有为ture时才能触发
if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
triggerUpdateProcessor();
}
}

//AdapterHelper.java
boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
if (itemCount < 1) {
return false;
}
mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
mExistingUpdateTypes |= UpdateOp.UPDATE;
//因此也就是说,只有第一次调用notifyItemxx才会返回ture
return mPendingUpdates.size() == 1;
}

请注意,onItemRangeChanged有个参数叫payload。
onItemRangeChanged除了通过返回ture/false来控制会不会走下一步外,还保存了一些参数如 UpdateOp.UPDATE 和 payload。
返回ture后->triggerUpdateProcessor->requestLayout();

同样的,调用了requestLayout后会重新绘制,即又会再次走到onLayoutChildren这些。但在此之前要先补充看下
跟前面一样也先从onMeasure入手->dispatchLayoutStep1->processAdapterUpdatesAndSetAnimationFlags

1
2
3
4
5
if (predictiveItemAnimationsEnabled()) {
mAdapterHelper.preProcess();
} else {
mAdapterHelper.consumeUpdatesInOnePass();
}

这里会先判断是否支持预测项目动画,3.0以上默认开启“预测项目动画”。
①如果开启支持的话会这样走:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void preProcess() {
mOpReorderer.reorderOps(mPendingUpdates);
final int count = mPendingUpdates.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
//这里的UPDATE就是前面提到的参数
case UpdateOp.UPDATE:
applyUpdate(op);
break;
}
}
mPendingUpdates.clear();
}

applyUpdate->postponeAndUpdateViewHolders(op)->markViewHoldersUpdated->viewRangeUpdate

1
2
3
4
5
6
7
8
9
10
if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
// We re-bind these view holders after pre-processing is complete so that
// ViewHolders have their final positions assigned.
// 请记住,此时给holder添加了一个FLAG_UPDATE的flag
holder.addFlags(ViewHolder.FLAG_UPDATE);
// 并且传入了一个payload
holder.addChangePayload(payload);
// lp cannot be null since we get ViewHolder from it.
((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
}

与payload有关的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void addChangePayload(Object payload) {
if (payload == null) {
addFlags(FLAG_ADAPTER_FULLUPDATE);
} else if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) {
createPayloadsIfNeeded();
mPayloads.add(payload);
}
}
private void createPayloadsIfNeeded() {
if (mPayloads == null) {
mPayloads = new ArrayList<Object>();
// 记住mUnmodifiedPayloads,后面要考到
mUnmodifiedPayloads = Collections.unmodifiableList(mPayloads);
}
}

②不支持开启“预测项目动画”,则是这样走:
mAdapterHelper.consumeUpdatesInOnePass,里面也会根据mPendingUpdates之前赋值的UpdateOp.UPDATE,最后走mCallback.markViewHoldersUpdated,这个刚刚也分析过了,就是把holder添加FLAG_UPDATE和payload。

所以不管怎么样,最后都是给holder增加FLAG_UPDATE和payload

结合我们上述说的layoutChildren一步步走,最后在scrapView那里

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
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);

mChangedScrap.add(holder);
}
}

boolean isUpdated() {
//此时的mFlags已经是FLAG_UPDATE了,所以会返回ture
return (mFlags & FLAG_UPDATE) != 0;
}
// 最终追溯到 DefaultItemAnimator.java
@Override
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull List<Object> payloads) {
// 这里的payload就是之前onItemRangeChanged那里传来的
return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
}

scrapView里面的条件判断,首先调用了notifyItemChanged的item一定会被标记update,所以holder.isUpdated返回了true,那么决定是把holder传到mAttachedScrap还是mChangedScrap就得看canReuseUpdatedViewHolder是否返回true。通过上面的代码发现了是跟payload是否为空有关。
即如果有payload,那么就会把holder保存到mAttachedScrap里面;没有payload,就会保存到mChangedScrap里面。

回到layoutChildren,接着就是fill这些,最终来到tryGetViewHolderForPositionByDeadline

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
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
// 0) If there is a changed scrap, try to find from there
// 如果是在预布局
// 就会从mChangedScrap里面拿
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
// 找不到,就从mAttachedViews和mCachedViews中找
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
……
}
……
if (holder == null) {
//如果所有缓存中都找不到
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
if (mState.isPreLayout() && holder.isBound()) {
// 预布局状态不用更新什么
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
// 如果holder是需要更新或者无效等等
// 需要重新绑定
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
}

private boolean tryBindViewHolderByDeadline(@NonNull RecyclerView.ViewHolder holder, int offsetPosition,
int position, long deadlineNs) {
holder.mOwnerRecyclerView = RecyclerView.this;
……
mAdapter.bindViewHolder(holder, offsetPosition);
……
return true;
}
public final void bindViewHolder(@NonNull VH holder, int position) {
……
onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());

}

// 这里就会用到前面讲的mUnmodifiedPayloads
// 也就是我们实际传的payload或者是空的
List<Object> getUnmodifiedPayloads() {
if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) {
if (mPayloads == null || mPayloads.size() == 0) {
// Initial state, no update being called.
return FULLUPDATE_PAYLOADS;
}
// there are none-null payloads
return mUnmodifiedPayloads;
} else {
// a full update has been called.
return FULLUPDATE_PAYLOADS;
}
}

当dispatchLayoutStep1时,开始preLayout,假设没有payload参数,那么holder就是从mChangedScrap里面拿,并且不会做更新绑定
当dispatchLayoutStep2时,开始postLayout
①如果没有payload参数,那么之前的缓存holder由于是保存在mChangedScrap里面,现在不是preLayout阶段,所以只能从recycledViewPool中拿(详情看前面recycleViewHolderInternal那部分的分析)。如果没有找到,就只能通过createHolder,然后再重新绑定adapter。
②如果有payload参数,那么之前的缓存holder就是保存在mAttachScrap里面,就可以从那个列表拿出来了。注意!此时holder.needsUpdate是true,所以会重新绑定adapter。所以如果没有notify时,保存到mAttachScrap里面的holder是不用重新绑定adapter的。如果调用了notify就需要重新绑定adapter了。

再一次看payload,它是从onItemRangeChanged来,而这又是从notifyItemChanged来:

1
2
3
4
5
6
public final void notifyItemChanged(int position, @Nullable Object payload) {
mObservable.notifyItemRangeChanged(position, 1, payload);
}
public final void notifyItemChanged(int position) {
mObservable.notifyItemRangeChanged(position, 1);
}

此外还有一点需要注意的是,adapter的onBindViewHolder也有两个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 当使用无payload的notify时,可以只复写这个
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
//完全刷新,比如这个item要换图片换标题文字等等
}
// 如果使用了带payload的notify,那就必须复写这个
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List payloads) {
if(payloads!=null){
// todo 实现局部刷新,比如只更新文字
}else {
onBindViewHolder(holder,position);
}
}

总结:
1.notifyDataSetChanged最终会重新创建viewHolder,而notifyItemChange是从mChangedScrape里面拿viewHolder。
2.即使你调用 notifyItemChanged 多次,其实只有第一次会触发 requestLayout() 。
3.如果想实现局部刷新,那么需要调用带有payload的notifyItemChanged(position,payload)

四、简单了解ListView的缓存机制
ListView的缓存只有两层:mActiveViews和mScrapViews。
mActiveViews:快速重用屏幕上可见的列表项,不需要重新createView和bindView;
mScrapViews:缓存离开屏幕的ItemView,目的是让即将进入屏幕的ItemView重用.
流程如下图:

ListView缓存机制
ListView缓存机制

参考自:
真正带你搞懂 RecyclerView 的缓存机制,再也不怕面试被虐了
RecyclerView局部刷新和原理介绍
换个姿势看源码 | RecyclerView预布局(pre-layout)
深入理解Android RecyclerView的缓存机制