RecyclerView的复用

RecyclerView的复用机制

前提

RecyclerView,即“熟悉”又“陌生”的控件。说起熟悉,是因为它频繁的使用在各个界面中,手机的竖直操作,需要大量的列表展示,导致其是最常用的控件(ViewGroup)之一。而陌生,也是因为完善的API和以前的ListView使用基本类似,强大的复用机制,高效的性能,上万行的代码,也都懒得再去研究它的原理。

所以,这篇文章虽说是复用机制的分析,不过在阅读的时候,可以不必死扣我贴出来的源码,主要还是理解思路。

本文阅读指南:源码可以随便看看,贴出来也是定位作用,别纠结😤。文字还是要看的😄,图片是配合记忆的🤔。

职责分析

回想一下RecyclerView的基本使用:
1、我们需要有一个RecyclerView控件
2、创建一个LayoutManager给RecyclerView
3、创建一个Adapter,Adapter中会返回我们自定义的ViewHolder
4、最后,给RecyclerView设置上Adapter就OK了
5、如果需要动画,可以添加ItemAnimator

在基本使用中发现RecyclerView有这几个小弟:LayoutManager,Adapter,ItemAnimator。而RecyclerView是大哥,是用来指挥小弟干活的,也是负责小弟互相沟通的。(ItemAnimator可以暂时不考虑)

RecyclerView职责图.png
  • LayoutManager:负责布局。负责子view的摆放,子view多大,放在哪里,都由它决定。所以RecyclerView可以很方便的切换列表、表格、流式布局
  • Adapter:提供view。负责提供子view(createViewHolder)和子view的数据更新(bindViewHolder)。至于view和viewHolder什么关系,我们在缓存中解释。
  • RecyclerView:负责管理。一个ViewGroup,是这些小弟的大哥,它负责显示,并让小弟各司其职的干活,把它当成包工头就行。

缓存和性能的体现

各个类职责明确,从不冲突,发生的问题都交给RecyclerView处理,再由RecyclerView分发给各个类处理。而缓存的存在,也大大提高了RecyclerView的性能。

接下来就按最常见的情况来具体分析各个类的职责和缓存在其中的作用。

缓存获取流程.png

先把各个类和缓存结合起来,场景的步骤按1、2、3的顺序标注在图上。

场景:手指开始滑动列表,一个新的Item需要显示在屏幕上。观察Item的完整创建。

  1. 手指拖动,RecyclerView接收到滑动事件,RecyclerView心想:我是大哥,让LayoutManager去干这事。

  2. LayoutManger接受到通知,发现需要一个新的View用来布局,调用getViewForPositon方法通知RecyclerView。

  3. RecyclerView开始去寻找缓存中是否存在新的View。如果存在,那最好了,直接把这个View返回,交给LayoutManager进行布局。当然,最开始的时候是没有的,所以RecyclerView没有从缓存中获取到View

  4. RecyclerView没有获取到View后,就拿这个新Item的position去问Adapter:这个Item是什么类型的啊?Adapter就会返回一个ItemType。

  5. RecyclerView就拿着ItemType再去另一个缓存(Recycled Pool)中查找该类型的View。如果存在,贼棒,直接返回了。当然了,最开始也是没有的,所以返回NO。

  6. RecyclerView发现所有的缓存中都没有我要的View,那就只能通知Adapter重新创建一个了。

如此这般,一个新的Item就创建出来交给了LayoutManager进行布局并渲染显示在了屏幕上。

RecyclerView的4级缓存

  • 一级缓存mAttachedScrap 和 mChangedScrap
    • mAttachedScrap:缓存或者存储当前还在屏幕上的viewHolder。
    • mChangedScrap:数据已经被改变的viewHolder。
  • 二级缓存mCachedViews:缓存移除屏幕之外的viewHolder,如果刚划出屏幕又往回拉,那就可以从这里获取,还是会根据position验证。
  • 三级缓存ViewCacheExtension:自定义的缓存,暂时忽略。
  • 四级缓存RecycledViewPool:缓存迟,会根据type分类存储。

缓存的性能当然也是从高到低排列,最好的情况应该是啥都不改,直接拿来放在屏幕上显示;最坏的情况应该是所有的缓存都没有找到,最终create一个View。

瞟一眼tryGetViewHolderForPositionByDeadline函数

现在就来到了缓存必贴的源码tryGetViewHolderForPositionByDeadline,该函数就是复用缓存的使用链,猜测应该是按照性能从高到低,和上图的模型基本一致:先从cache中寻找,再去pool中寻找,最后是Adapter创建。

这里有个疑点:Adapter创建很好理解,不过cache和pool有什么区别???🤔️(带着这个疑问继续看源码)

tryGetViewHolderForPositionByDeadline()

// 0) If there is a changed scrap, try to find from there
// preLayout?暂时没理解,先忽略
if (mState.isPreLayout()) {
    holder = getChangedScrapViewForPosition(position);
    fromScrapOrHiddenOrCache = holder != null;
}

// 1) Find by position from scrap/hidden list/cache
// 根据position寻找hodler
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);

// 2) Find from scrap/cache via stable ids, if exists
// 默认情况返回false,可暂时忽略
if (mAdapter.hasStableIds()) {
    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
            type, dryRun);
}

// 3) 使用mViewCacheExtension,自定义的缓存,平常用不到,也忽略

// 4) 从RecycledViewPool中寻找
holder = getRecycledViewPool().getRecycledView(type);

// 5) Adapter创建一个新的ViewHolder
holder = mAdapter.createViewHolder(RecyclerView.this, type);

首次看源码:

  • 第0次查找:发现preLayout不知道干什么,但里面去mChangedScrap寻找holder,不理解没关系,后面有需要再回来看;

  • 第2次查找:mAdapter.hasStableIds()默认返回false,这个只需要向上追溯,其中的变量mHasStableIds默认就是false,而设置true需要调用Adapter.setHasStableIds(),所以也暂时忽略,后面有需要再回来看;

  • 第3次查找:使用mViewCacheExtension,这是个抽象类,基本是给开发者扩展使用,它的使用也是在cachepool之间,很少用到,暂时也忽略,在性能Tips中会提到。

  • 第5次查找:沿着顺序查找下来,holder还是空,只能让Adapter创建,这已经是最差的情况,性能也是最低的。

上面能忽略的忽略,能理解的理解,也就还剩第1次查找和第4次查找需要着重分析了。

还记得咱们的疑问吗:cache和pool有什么区别???🤔️

getScrapOrHiddenOrCachedHolderForPosition

先看第1次查找getScrapOrHiddenOrCachedHolderForPosition

唉,forPosition,说明是按照Item的位置来查找的。

// Try first for an exact, non-invalid match from scrap.
// 尝试从scrap中匹配准确的有效的Item
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;
    }
}

一个循环,从mAttachedScrap中寻找,需要满足好多条件:wasReturnedFromScrap(从scrap中返回)、isInvalid(无效)、isRemoved(被移除)

boolean wasReturnedFromScrap() {
    return (mFlags & FLAG_RETURNED_FROM_SCRAP) != 0;
}
boolean isInvalid() {
    return (mFlags & FLAG_INVALID) != 0;
}
boolean isRemoved() {
    return (mFlags & FLAG_REMOVED) != 0;
}

其中有判断position是否一致。对mFlags还是比较模糊,虽然能从函数中看个大概意思,但却不知道它何时为true何时为false。

// Search in our first-level recycled view cache.
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
            && !holder.isAttachedToTransitionOverlay()) {
        if (!dryRun) {
            mCachedViews.remove(i);
        }
        if (DEBUG) {
            Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                    + ") found match in cache: " + holder);
        }
        return holder;
    }
}

嗯,比较晕,if中这么多判断,统统先扔一边,看多了越来越绕😵

getRecycledViewPool().getRecycledView(type)

再看一下getRecycledViewPool

public static class RecycledViewPool {
    private static final int DEFAULT_MAX_SCRAP = 5;
    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<>();

RecycledViewPool是一个SparseArray<ScrapData>
ScrapData里面又有一个ArrayList<ViewHolder>,size = 5;
来个图,方便理解吧

RecycledViewPool数据结构.png

pool就长这个样,通过ViewType找到同类型的集合,从中取个最新的holder缓存。

这样就算是从pool中取出来了。不过还有注意点,回到tryGetViewHolderForPositionByDeadline函数中。

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

从pool中获取到holder后,还需要进行判断,对holder进行重置,这是cache中没有的。

void resetInternal() {
    mFlags = 0;
    mPosition = NO_POSITION;
    mOldPosition = NO_POSITION;
    mItemId = NO_ID;
    mPreLayoutPosition = NO_POSITION;
    mIsRecyclableCount = 0;
    mShadowedHolder = null;
    mShadowingHolder = null;
    clearPayload();
    mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
    mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
    clearNestedRecyclerViewIfNotNested(this);
}

经过5次查找之后,最坏的情况是holder被create出来。

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);
    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}

holder被重置或创建后,isBound()返回false,也不用看其他,妥妥的进了else if,直接对holder进行bind。

从获取的方法名中也能看出,因为cache中获取到的是根据position获取的,而pool中这是根据type获取,也间接说明pool中的Item需要被重新绑定。

那再来看看何时把cache放到pool中

int mViewCacheMax = DEFAULT_CACHE_SIZE;
static final int DEFAULT_CACHE_SIZE = 2;

int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
    recycleCachedViewAt(0);
    cachedViewSize--;
}

mCachedViews数量就2个,超过了,就把旧的扔进pool中。

缓存小结

那么再回到最初的问题:cache和pool有什么区别???🤔️

cache因为复用性能较高,所以根据position获取,基本可以直接使用,不过也会对获取到的holder进行验证(就是这么严格),一旦发现holder的验证失效,此时它的数据是“脏的”,这时就需要把它放到pool中;

而pool根据type存储,又会接受cache的一些无效holder,内部数据已经“脏了”,必须要重置,所以需要bind数据;

RecyclerView内部的逻辑和考虑的情况特别多,所以好些mFlag都略过不提,只是想办法把cache和pool区分,在此也只是抛砖引玉,大佬帮忙指正。

参考

RecyclerView缓存机制(咋复用?) - 掘金
RecyclerView缓存机制(回收些啥?) - 掘金
RecyclerView缓存机制(回收去哪?) - 掘金
RecyclerView缓存机制(scrap view) - 掘金
RecyclerView 的缓存复用机制
每日一问 | RecyclerView的多级缓存机制,每级缓存到底起到什么样的作用?
RecyclerView ins and outs - Google I/O 2016 - YouTube

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342