RecyclerView缓存机制

February 13, 2019

一、RecyclerView与性能

使用 RecyclerView 的难度可大可小,若仅展示单类型列表,只要优化视图绘制性能、降低布局复杂度,就能保证性能。

若列表分类多、样式差异大,类似微信聊天消息界面,遇到问题的难度将大幅增加。需要在预加载、复用上做进一步调优,单纯实现 onCreateViewHolder()onBindViewHolder() 并不能满足需求。总的来说,要以追求视图出现在屏幕前耗费最少时间为目标。

RecyclerView 缓存分为3级,每级有各自的缓存数量和策略。

RecyclerView_cache_level

当所有缓存层均没有所需实例,最后由 onCreateViewHolder() 创建并绑定数据。源码版本:Android 27.1.1

二、一级缓存

一级缓存包含三个容器实例:mAttachedScrapmChangedScrapmCachedViews。根据不同场景 ViewHolder 缓存到不同容器。

mAttachedScrap 保存依附于 RecyclerViewViewHolder。包含移出屏幕但未从 RecyclerView 移除的 ViewHolder

1
final ArrayList<RecyclerView.ViewHolder> mAttachedScrap = new ArrayList();

mChangedScrap 保存数据发生改变的 ViewHolder,即调用 notifyDataSetChanged() 等系列方法后需要更新的 ViewHolder

1
ArrayList<RecyclerView.ViewHolder> mChangedScrap = null;

mCachedViews 用于解决滑动抖动的问题,默认容量为2,可根据需要调优。

1
final ArrayList<RecyclerView.ViewHolder> mCachedViews = new ArrayList();

三、二级缓存

开发者自定义的缓存,需实现 ViewCacheExtension 抽象类。若没有定义此缓存默认为null。

1
private RecyclerView.ViewCacheExtension mViewCacheExtension;

四、三级缓存

mCachedViews 无法保存屏幕上所有移除的 ViewHolder 时,剩余的 ViewHolder 根据 type 分类放入缓存池中。

1
RecyclerView.RecycledViewPool mRecyclerPool;

五、Recycler

5.1 tryGetViewHolderForPositionByDeadline()

以下是 RecyclerView 的内部类 Recycler 去除类签名的源码。Adapter 利用position获取 ViewHolder:若一级缓存命失、mViewCacheExtension 为空,则从缓存池查找对象。

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    // 越界检查
    if (position < 0 || position >= mState.getItemCount()) {
        throw new IndexOutOfBoundsException("Invalid item position " + position
                + "(" + position + "). Item count:" + mState.getItemCount()
                + exceptionLabel());
    }
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;

    // 0) 从mChangedScrap获取ViewHolder
    if (mState.isPreLayout()) {
        // 用position作为参数
        holder = getChangedScrapViewForPosition(position);
        // from scrap.
        fromScrapOrHiddenOrCache = holder != null;
    }

    // 1) 用position从scrap、hidden list、cache中获取
    if (holder == null) {
        // 从第一级缓存中获取,从mAttachedScrap和mCachedViews中获取ViewHolder
        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;
            }
        }
    }
    
    // 用posotion查找缓存失败,尝试使用stable ids获取缓存
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
            throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                    + "position " + position + "(offset:" + offsetPosition + ")."
                    + "state:" + mState.getItemCount() + exceptionLabel());
        }

        // 用offsetPosition获取ViewType
        final int type = mAdapter.getItemViewType(offsetPosition);
        if (mAdapter.hasStableIds()) {
            // 2) 通过stable ids和type从scrap/cache查找
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            if (holder != null) {
                // 更新position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
            }
        }
        
        // 从第二级缓存ViewCacheExtension中获取缓存内容,即mViewCacheExtension
        if (holder == null && mViewCacheExtension != null) {
            // We are NOT sending the offsetPosition because LayoutManager does not
            // know it.
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
                if (holder == null) {
                    throw new IllegalArgumentException("getViewForPositionAndType returned"
                            + " a view which does not have a ViewHolder"
                            + exceptionLabel());
                } else if (holder.shouldIgnore()) {
                    throw new IllegalArgumentException("getViewForPositionAndType returned"
                            + " a view that is ignored. You must call stopIgnoring before"
                            + " returning this view." + exceptionLabel());
                }
            }
        }
        
        // 从三级缓存RecycledViewPool中获取缓存内容
        // ViewHolder内layout可重用,但是数据需重新绑定
        if (holder == null) {
            // 根据目标类型获取ViewHolder
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        
        // 从所有缓存中均没法获取对应数据,只能通过Adapter构造新的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;
            }
            // 这里调用继承Adapter后实现的createViewHolder方法
            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);
            }
        }
    }

    // This is very ugly but the only place we can grab this information
    // before the View is rebound and returned to the LayoutManager for post layout ops.
    // We don't need this in pre-layout since the VH is not updated by the LM.
    if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
            .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
        holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
        if (mState.mRunSimpleAnimations) {
            int changeFlags = ItemAnimator
                    .buildAdapterChangeFlagsForAnimations(holder);
            changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
            final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
                    holder, changeFlags, holder.getUnmodifiedPayloads());
            recordAnimationInfoIfBouncedHiddenView(holder, info);
        }
    }

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // do not update unless we absolutely have to.
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        if (DEBUG && holder.isRemoved()) {
            throw new IllegalStateException("Removed holder should be bound and it should"
                    + " come here only in pre-layout. Holder: " + holder
                    + exceptionLabel());
        }
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        // 以下方法调用mAdapter.bindViewHolder把内容绑定到ViewHolder
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }

    final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    final LayoutParams rvLayoutParams;
    if (lp == null) {
        rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else if (!checkLayoutParams(lp)) {
        rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else {
        rvLayoutParams = (LayoutParams) lp;
    }
    rvLayoutParams.mViewHolder = holder;
    rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
    return holder;
}

5.2 getScrapOrHiddenOrCachedHolderForPosition()

attach scraphidden childrencache 根据 position 返回 ViewHolder

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
// @param position 条目位置
// @param dryRun   空转,只查找ViewHolder而不移除
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.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            return holder;
        }
    }

    if (!dryRun) {
        View view = mChildHelper.findHiddenNonRemovedView(position);
        if (view != null) {
            // This View is good to be used. We just need to unhide, detach and move to the
            // scrap list.
            final ViewHolder vh = getChildViewHolderInt(view);
            mChildHelper.unhide(view);
            int layoutIndex = mChildHelper.indexOfChild(view);
            if (layoutIndex == RecyclerView.NO_POSITION) {
                throw new IllegalStateException("layout index should not be -1 after "
                        + "unhiding a view:" + vh + exceptionLabel());
            }
            mChildHelper.detachViewFromParent(layoutIndex);
            scrapView(view);
            vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                    | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
            return vh;
        }
    }

    // 从一级缓存查找已回收的视图缓存
    final int cacheSize = mCachedViews.size();
    for (int i = 0; i < cacheSize; i++) {
        final ViewHolder holder = mCachedViews.get(i);
        // invalid view holders may be in cache if adapter has stable ids as they can be
        // retrieved via getScrapOrCachedViewForId
        if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
            if (!dryRun) {
                mCachedViews.remove(i);
            }
            return holder;
        }
    }
    return null;
}

5.3 tryBindViewHolderByDeadline()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition,
        int position, long deadlineNs) {
    // 设置holder的RecyclerView
    holder.mOwnerRecyclerView = RecyclerView.this;
    // holder的viewType
    final int viewType = holder.getItemViewType();
    long startBindNs = getNanoTime();
    if (deadlineNs != FOREVER_NS
            && !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) {
        // abort - we have a deadline we can't meet
        return false;
    }
    // 绑定视图数据
    mAdapter.bindViewHolder(holder, offsetPosition);
    long endBindNs = getNanoTime();
    mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);
    attachAccessibilityDelegateOnBind(holder);
    if (mState.isPreLayout()) {
        holder.mPreLayoutPosition = position;
    }
    return true;
}

六、RecycledViewPool

若要把 RecycledViewPool 在多个 RecyclerViews 间共享,只需主动创建该实例,通过 RecyclerView.setRecycledViewPool(RecycledViewPool) 绑定到 RecyclerView 上。如果没有给 RecyclerView 指定任何 RecycledViewPool,则会自行创建该实例。

每个 type 默认缓存5个 ViewHolder,可针对不同 type 修改需缓存数量。例如:增加显示面积较小 ViewHolder 缓存的数量,保证缓存对象足够填满屏幕又无需创建新对象。

1
private static final int DEFAULT_MAX_SCRAP = 5;

ScrapDataRecycledViewPool 的内部类

1
2
3
4
5
6
7
8
9
10
static class ScrapData {
    // 保存ViewHolder的列表
    final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
    // 本类型最多可保留多少ViewHolder
    int mMaxScrap = DEFAULT_MAX_SCRAP;
    // 记录创建视图的平均时长
    long mCreateRunningAverageNs = 0;
    // 记录视图绑定的平均时长
    long mBindRunningAverageNs = 0;
}

根据分类缓存 ScrapData

1
SparseArray<ScrapData> mScrap = new SparseArray<>();

调整指定 viewType 视图缓存的最大值

1
2
3
4
5
6
7
8
9
10
public void setMaxRecycledViews(int viewType, int max) {
    ScrapData scrapData = getScrapDataForType(viewType);
    // 修改mMaxScrap的值
    scrapData.mMaxScrap = max;
    final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
    // 裁剪多余缓存实例
    while (scrapHeap.size() > max) {
        scrapHeap.remove(scrapHeap.size() - 1);
    }
}

例如:下图样式的 ViewHolder 仅缓存5个,多余视图移出屏幕后会销毁。下次需要该 ViewHolder 又要重新构建,所以提高缓存数量可减少这种情况发生次数。

RecyclerView_demo

根据 viewType 从缓存池获取 ScrapData,再从 ScrapData 取出有效 ViewHolder。如果缓存池内没有缓存该实例则返回null。

1
2
3
4
5
6
7
8
9
10
@Nullable
public ViewHolder getRecycledView(int viewType) {
    final ScrapData scrapData = mScrap.get(viewType);
    if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
        final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
        // 从列表取出一个ViewHolder
        return scrapHeap.remove(scrapHeap.size() - 1);
    }
    return null;
}

读取 ViewHolderviewType 并找到对应 scrapHeap 列表,把 ViewHolder 缓存到该列表

1
2
3
4
5
6
7
8
9
public void putRecycledView(ViewHolder scrap) {
    final int viewType = scrap.getItemViewType();
    final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
    if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
        return;
    }
    scrap.resetInternal();
    scrapHeap.add(scrap);
}

根据 viewType 获取 ScrapData

1
2
3
4
5
6
7
8
private ScrapData getScrapDataForType(int viewType) {
    ScrapData scrapData = mScrap.get(viewType);
    if (scrapData == null) {
        scrapData = new ScrapData();
        mScrap.put(viewType, scrapData);
    }
    return scrapData;
}

七、参考链接