RecyclerView的缓存分析

RecyclerView的缓存主要体现在RecyclerView的内部类Recycler

重要的成员变量

四级缓存 —— Scrap、Cache、ViewCacheExtension 、RecycledViewPool

  1. mAttachedScrap
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
  2. mChangedScrap
    ArrayList<ViewHolder> mChangedScrap = null;
  3. mCachedViews
    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
  4. ViewCacheExtension
    private ViewCacheExtension mViewCacheExtension;
  5. RecycledViewPool
    RecycledViewPool mRecyclerPool;

Cache缓存默认大小

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

重要的方法

设置Cache缓存大小 —— setViewCacheSize

这个方法是公有的,所以可以在自己的RecyclerView中定义Cache缓存大小,例如:

mRecyclerView.setItemViewCacheSize(5);

获得指定位置的子View —— getViewForPosition,可能来自于缓存,也可能重新创建

搜索mChangedScrap列表,从对应postion中找,找不到再从对应id找

只有满足mState.isPreLayout()这个条件才会搜索mChangedScrap列表,这个条件在dispatchLayoutStep1中赋值为mState.mInPreLayout = mState.mRunPredictiveAnimations;,即发生添加、删除、修改要执行动画效果时,mState.mInPreLayout为true;在dispatchLayoutStep2中会赋值为false。显然只有在dispatchLayoutStep1中要执行动画的时候会调用mLayout.onLayoutChildren(mRecycler, mState);方法,预布局时,getViewForPosition才会走这第一步。

if (mState.isPreLayout()) {
    holder = getChangedScrapViewForPosition(position);
    fromScrapOrHiddenOrCache = holder != null;
}
  1. 找是否有和postion相同的holder
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
    return holder;
}
  1. 找是否有和id相同的holder
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
    return holder;
}

通过postion按顺序搜索mAttachedScrap、ChildHelper中存的mHiddenViews、mCachedViews列表

if (holder == null) {
    holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
  1. 搜索mAttachedScrap
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
        && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
    return holder;
}
  1. 搜索mHiddenViews,从mHiddenViews找到相应的holder后,立即将其从mHiddenViews中移除,然后添加到Scrap缓存中
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
    // This View is good to be used. We just need to unhide, detach and
    // scrap list.
    final ViewHolder vh = getChildViewHolderInt(view);
    mChildHelper.unhide(view);
    int layoutIndex = mChildHelper.indexOfChild(view);
    mChildHelper.detachViewFromParent(layoutIndex);
    scrapView(view);
    vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
            | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
    return vh;
}
  1. 搜索mCachedViews,从Cache缓存中找到相应的holder后,立即将其从Cache缓存中移除
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
    if (!dryRun) {
        mCachedViews.remove(i);
    }
    return holder;
}

通过id按顺序搜索mAttachedScrap、mCachedViews列表

if (mAdapter.hasStableIds()) {
    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
            type, dryRun);
}
  1. 搜索mAttachedScrap
if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
    if (type == holder.getItemViewType()) {
        holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
        if (holder.isRemoved()) {
            // this might be valid in two cases:
            // > item is removed but we are in pre-layout pass
            // >> do nothing. return as is. make sure we don't rebind
            // > item is removed then added to another position and we are in
            // post layout.
            // >> remove removed and invalid flags, add update flag to rebind
            // because item was invisible to us and we don't know what happened in
            // between.
            if (!mState.isPreLayout()) {
                holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE |
                        ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
            }
        }
        return holder;
    } else if (!dryRun) {
        // if we are running animations, it is actually better to keep it in scrap
        // but this would force layout manager to lay it out which would be bad.
        // Recycle this scrap. Type mismatch.
        mAttachedScrap.remove(i);
        removeDetachedView(holder.itemView, false);
        quickRecycleScrapView(holder.itemView);
    }
}
  1. 搜索mCachedViews
if (holder.getItemId() == id) {
    if (type == holder.getItemViewType()) {
        if (!dryRun) {
            mCachedViews.remove(i);
        }
        return holder;
    } else if (!dryRun) {
        recycleCachedViewAt(i);
        return null;
    }
}

用户通过ViewCacheExtension可以自定义缓存策略

final View view = mViewCacheExtension
        .getViewForPositionAndType(this, position, type);

通过RecycledViewPool获取缓存

需要注意的是,如果从RecycledViewPool中获取到了相应的holder,要将holder的一些状态重置,因为从这取的holder只是根据type匹配的,不是position对应的holder,所以需要重置holder的状态

holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
    holder.resetInternal();
    if (FORCE_INVALIDATE_DISPLAY_LIST) {
        invalidateDisplayListInt(holder);
    }
}

如果上述步骤都没获取到值,则通过Adapter的createViewHolder方法创建一个holder

holder = mAdapter.createViewHolder(RecyclerView.this, type);

调用Adapter的bindViewHolder方法,会调用Adapter的onBindViewHolder空方法

只有满足这三个条件之一,才会调用Adapter的bindViewHolder方法

  1. holder还没绑定,即还没调用bindViewHolder方法,这个是唯一能将holder的标记设为绑定的方法
  2. holder需要更新
  3. holder已经无效
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);
    }
    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}

总的来说分为这四类缓存:


Scrap缓存

scrap缓存主要用在布局前后,主要包括mAttachedScrap和mChangedScrap这两个缓存列表

添加缓存 —— scrapView

if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
        || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
    holder.setScrapContainer(this, false);
    mAttachedScrap.add(holder);
} else {
    if (mChangedScrap == null) {
        mChangedScrap = new ArrayList<ViewHolder>();
    }
    holder.setScrapContainer(this, true);
    mChangedScrap.add(holder);
}

添加Scrap缓存的时机

每当RecyclerView调用dispatchLayoutStep2方法,内部都会调用onLayoutChildren方法,虽然不同的LayoutManager的实现不同,但是其中都会调用detachAndScrapAttachedViews方法,在这个方法中会对RecyclerView中已经添加的子View遍历调用scrapOrRecycleView方法,scrapOrRecycleView方法会根据holder的状态来判断是要添加到cache缓存中还是scrap缓存中,如果添加到Scrap缓存,最终会调用scrapView方法

大多数情况会添加到mAttachedScrap这个Scrap缓存中,什么时候会添加到mChangedScrap缓存中呢?举个例子:

update_C.png

如上图的列表,我现在要将字母C修改为Z,当调用getAdapter().notifyItemChanged(2);方法,流程如下

Recycler_Update_Flow.png

也就是说要修改的item会添加到mChangedScrap缓存中去,其余的会添加到mAttachedScrap缓存中

移除缓存 —— unscrapView

根据添加缓存方法中holder.setScrapContainer(this, boolean);这行代码设置的boolean值来判断,true则移除mChangedScrap中的holder,false则移除mAttachedScrap中的holder

移除Scrap缓存的时机:

  1. RecyclerView的addView方法,内部会根据holder.wasReturnedFromScrap() || holder.isScrap()此条件判断是否需要移除scrap缓存,相应的会attach之前添加scrap缓存时detach的viewmChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
  2. onLayout中最终会调用dispatchLayoutStep3方法,内部调用了removeAndRecycleScrapInt方法回收所有的scrap缓存

显然,Scrap缓存只是用在布局期间,布局后就清空了Scrap缓存

Cache缓存

添加缓存 —— recycleViewHolderInternal

在 RecyclerView中通过recycleViewHolderInternal方法添加缓存

  1. 满足以下两个条件,才能添加到Cache缓存中:
    a. mViewCacheMax > 0,即Cache缓存设置的大小要大于0
    b. !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN),一般指的是该item不会执行动画,例如滑动中等
  2. 如果超过Cache缓存的最大,则移除第0个缓存
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
    recycleCachedViewAt(0);
    cachedViewSize--;
}
  1. 添加到Cache缓存的最后
int targetCacheIndex = cachedViewSize;
mCachedViews.add(targetCacheIndex, holder);
cached = true;

显然,Cache缓存的数据结构是后入先出的队列结构

添加Cache缓存时机

dispatchLayoutStep2

  1. 在通过fill方法填充布局时,会遍历每一个ChildHelper中的子类,如果满足viewHolder.isInvalid() && !viewHolder.isRemoved() &&!mRecyclerView.mAdapter.hasStableIds()这个条件,则会添加到cache缓存中去
  2. 在通过getViewForPosition方法获得给定位置的itemview时,假如通过第二步getScrapOrHiddenOrCachedHolderForPosition方法获得了一个holder,满足!validateViewHolderForOffsetPosition(holder)条件,则会添加到cache缓存中去
  3. 调用recycleView方法,将holder回收到cache缓存中

dispatchLayoutStep3

  1. removeAnimatingView中,如果是从ChildHelper的mHiddenViews中找到并移除了这个View,则将这个View添加到cache缓存中去,举个例子,当调用notifyItemRangeRemoved方法删除item,则被删除的item在执行完删除动画,会将这个item的holder添加到cache缓存中
  2. removeAndRecycleScrapInt中,清空Scrap缓存,并将其添加到Cache缓存中

除了上述几种情况,对于不同的LayoutManager还有不同的区别,例如LinearLayoutManager调用fill方法时,在方法开头会调用recycleByLayoutState方法,该方法会回收看不到的item

注意:子View回收之前必须已经从父布局中detached或removed

移除缓存 —— mCachedViews.remove

  1. 在getViewForPosition步骤2时,通过getScrapOrHiddenOrCachedHolderForPosition方法,从cache缓存中获取到了holder,则移除
  2. 在getViewForPosition步骤3时,通过getScrapOrCachedViewForId方法,从cache缓存中获取到了holder,则移除
  3. 需要通过recycleCachedViewAt方法移除cache缓存时

ViewCacheExtension

用户可以自定义的缓存

RecyclerViewPool

添加到RecyclerViewPool中 —— putRecycledView

scrapHeap.add(scrap);

从RecyclerViewPool中获取 —— getRecycledView

return scrapHeap.remove(scrapHeap.size() - 1);

从添加和获取缓存来看,RecyclerViewPool的数据结构是后进先出的栈结构,这能保证每次获取到的holder都是池中最新的

添加RecyclerViewPool缓存 —— recycleViewHolderInternal

recycleViewHolderInternal内部,如果没有将item添加到Cache缓存中,则会添加到RecyclerViewPool缓存中

// cache的值在添加Cache缓存的步骤中赋值
if (!cached) {
    addViewHolderToRecycledViewPool(holder, true);
    recycled = true;
}

添加RecyclerViewPool缓存时机

由于在RecyclerView中添加Cache缓存和RecyclerViewPool缓存用的是同一个方法recycleViewHolderInternal,所以两个缓存的添加时机是一样的

移除缓存 —— getViewForPosition

return scrapHeap.remove(scrapHeap.size() - 1);

只在getViewForPosition时,从RecyclerViewPool缓存中获取到holder,同时从RecyclerViewPool中移除

RecyclerView各种状态下的缓存分析

加载RecyclerView显示到屏幕上

在dispatchLayoutStep2中的缓存变动

所在方法 缓存类型 列表中的数据
ChildHelper,即ReclyerView中的子View
ChildHelper_List.png
detachAndScrapAttachedViews Scrap缓存的mAttachedScrap列表
mAttachedScrap.png
getViewForPosition 从Scrap缓存的mAttachedScrap列表中取
addView 从Scrap缓存的mAttachedScrap列表中移除缓存

滑动RecyclerView

可以打印Cache缓存列表和RecyclerViewPool缓存列表来看滑动RecyclerView时的缓存变化,如下:

mRvTest.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
    }
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        mRvTest.printAllValue();
    }
});

public void printAllValue() {
    try {
        Field field = rvClz.getDeclaredField("mRecycler");
        field.setAccessible(true);
        recycler = (RecyclerView.Recycler) field.get(this);
        recycler.getScrapList();
        getCacheField("mCachedViews");
        Field poolField = recyclerPoolClz.getDeclaredField("mScrap");
        poolField.setAccessible(true);
        SparseArray<Object> sa = (SparseArray<Object>) poolField.get(this.getRecycledViewPool());
        for (int i = 0; i < sa.size(); i++) {
            Field fd = scrapDataClz.getDeclaredField("mScrapHeap");
            fd.setAccessible(true);
            ArrayList<ViewHolder> mScrapHeap = (ArrayList<RecyclerView.ViewHolder>) fd.get(sa.get(sa.keyAt(i)));
            for (int j = 0; j < mScrapHeap.size(); j++) {
                Log.d(TAG, "RecycledViewPool position: " + ((TextView) mScrapHeap.get(i).itemView.findViewById(R.id.tv)).getText());
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public void getCacheField(String fieldName) throws Exception {
    Field field = recyclerClz.getDeclaredField(fieldName);
    field.setAccessible(true);
    List<ViewHolder> viewHolders = (List<RecyclerView.ViewHolder>) field.get(recycler);
    if (viewHolders == null)
        return;
    for (int i = 0; i < viewHolders.size(); i++) {
        Log.d(TAG, fieldName + " position : " + ((TextView) viewHolders.get(i).itemView.findViewById(R.id.tv)).getText());
    }
}

如果RecyclerView滑动到F开始有一部分显示到屏幕中,则会

所在方法 缓存类型 列表中的数据
GapWorker.prefetchPositionWithDeadline Cache缓存
Cache缓存列表1.png
getViewForPosition 从Cache缓存的列表中取

如果RecyclerView滑动到A开始消失在屏幕中,则会

所在方法 缓存类型 列表中的数据
GapWorker.prefetchPositionWithDeadline Cache缓存
Cache缓存列表2.png
getViewForPosition 从Cache缓存的列表中取

总结

  1. Scrap缓存用在RecyclerView布局时,布局完成之后就会清空

  2. 添加到Cache缓存和RecyclerViewPool缓存的item,他们的View必须已经从RecyclerView中detached或removed

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