RecyclerView的缓存机制和内存优化

RecyclerView 缓存需要用到的数据结构在 Recycler 类里面.

    public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;

        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();//默认大小是2

        RecycledViewPool mRecyclerPool;

        private ViewCacheExtension mViewCacheExtension;
    }

这里面主要介绍一下 mAttachedScrap 和 mChangedScrap.他们都是在同一个函数中 add 的

        void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            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);
            }
        }

可知,已经被删除的或者不需要更新的的在 mAttachedScrap 中,未删除且需要更新的在 mChangedScrap 中.
(scrap 是废除的意思,那废除的 view 怎么回去添加到 mAttachedScrap 中呢?其实这个 mAttachedScrap 表示的是从屏幕上分离出来,但是又即将添加到屏幕上去的 view。比如说,RecyclerView 上下滑动,滑出一个新的 Item,此时会重新调用 LayoutManager 的 onLayoutChildren 方法,从而会将屏幕上所有的 view 先 scrap 掉,添加到 mAttachedScrap 里面去,然后重新布局的时候会从优先 mAttachedScrap 里面获取)

复用

RecyclerView 对 ViewHolder 的复用,我们得从 LayoutState 的 next 方法开始。最终来分析 RecyclerView 的 tryGetViewHolderForPositionByDeadline

        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
            ......
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            // 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
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if (holder != null) {
                    if (!validateViewHolderForOffsetPosition(holder)) {
                        if (!dryRun) {
                            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;
                    }
                }
            }
            if (holder == null) {
                ......

                final int type = mAdapter.getItemViewType(offsetPosition);
                // 2) Find from scrap/cache via stable ids, if exists
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
                }
                if (holder == null && mViewCacheExtension != null) {
                    final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
                    if (view != null) {
                        holder = getChildViewHolder(view);
                    }
                }
                if (holder == null) { // fallback to pool
                    holder = getRecycledViewPool().getRecycledView(type);
                    ......
                }
                if (holder == null) {
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    ......
                }
            }
            ......
            return holder;
        }

整个函数内容比较多.我们分步骤来看

step0
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }

当在 prelayout 阶段时, 走 getChangedScrapViewForPosition
(prelayout 是什么? https://www.jianshu.com/p/61fe3f3bb7ec 这里有提到,简单点就是 dispatchLayoutStep1. 要想真正开启 prelayout, 就和 processAdapterUpdatesAndSetAnimationFlags 中的 mRunSimpleAnimations 和 mRunPredictiveAnimations 参数有关)
接下来分析 getChangedScrapViewForPosition,很简单,就是从 mChangedScrap 寻找 viewhoder

step1
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if (holder != null) {
                    if (!validateViewHolderForOffsetPosition(holder)) {
                        if (!dryRun) {
                            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)。
另外,有必要提一下 mHiddenViews.它在 ChildHelper 中.如果当前操作支持动画,就会调用到 RecyclerView 的 addAnimatingView 方法,在这个方法里面会将做动画的那个 View 添加到 mHiddenView 数组里面去。通常就是动画期间可以会进行复用,因为 mHiddenViews 只在动画期间才会有元素。所以在整个复用流程里可以不用考虑

step2
                final int type = mAdapter.getItemViewType(offsetPosition);
                // 2) Find from scrap/cache via stable ids, if exists
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
                }
                if (holder == null && mViewCacheExtension != null) {
                    final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
                    if (view != null) {
                        holder = getChildViewHolder(view);
                    }
                }
                if (holder == null) { // fallback to pool
                    holder = getRecycledViewPool().getRecycledView(type);
                    ......
                }
                if (holder == null) {
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    ......
                }

流程很简单,根据 id 从 mAttachedScrap 和 mCachedViews 中获取数据.然后从 mViewCacheExtension 中获取, 然后 mRecyclerPool ,然后 createViewHolder.
注意两点,一个是 getScrapOrCachedViewForId 的前置条件 hasStableIds.后面会说.还有一个是 ViewCacheExtension.它是一个 abstract 类,看注释就知道其实就是复用的已经存在的 view,应该是预留给后续开发用的.
总结下来整个流程就是

  • 1.prelayout 下在 mChangedScrap 中寻找(需要更新的 viewhoder)
  • 2.分别从mAttachedScrap、 mCachedViews 获取 ViewHolder
    如果获取的 ViewHolder 是无效的,得做一些清理操作,然后重新放入到缓存里面,具体对应的缓存就是 mCacheViews 和 RecyclerViewPool
    ------上面是position,下面是type
  • 3.hasStableIds == true,根据 id 从 mAttachedScrap 和 mCachedViews 中获取数据
  • 4.从 mRecyclerPool 中获取(大小默认是 5)
    实在找不到,就 createViewHolder

hasStableIds

接下来看一下这个方法
复用时候的代码

                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    ......
                }

回收时候的代码

        private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                    && !mRecyclerView.mAdapter.hasStableIds()) {//TODO 这里
                removeViewAt(index);
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
                detachViewAt(index);
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

由此可见,当 hasStableIds = true 时,所有的 item 都进入 scrap 中,相当于提升了复用的效率

onViewRecycled

这个可以自己设置,也可以放在 Adapter 自己扩展.一般都是自己扩展
为啥专门提这个呢?看代码

        void dispatchViewRecycled(ViewHolder holder) {
            if (mRecyclerListener != null) {
                mRecyclerListener.onViewRecycled(holder);
            }
            if (mAdapter != null) {
                mAdapter.onViewRecycled(holder);
            }
            if (mState != null) {
                mViewInfoStore.removeViewHolder(holder);
            }
            if (DEBUG) Log.d(TAG, "dispatchViewRecycled: " + holder);
        }

接下来去寻找调用它的地方,只有一个地方 addViewHolderToRecycledViewPool.在往上就两处

  • 1.recycleViewHolderInternal。 这里应该知道是啥
  • 2.recycleCachedViewAt------从代码看出,recycleCachedViewAt 就是把 mCachedViews 中的数据移除然后放到 RecycledViewPool 中

说白了就是在回收的时候会出现 onViewRecycled 的调用.另外,是在 RecycledViewPool 之前.而我们知道被回收不可见时第一次选择是放进 mCacheView中,但是这里面的 item 被复用时并不会去执行 bindViewHolder 来重新绑定数据,只有被回收进 mRecyclePool 后拿出来复用才会重新绑定数据。所以此时我们应该在 item 被回收进 RecyclePool 的时候去释放图片的引用.注意,此时 hasStableIds 是 false.
所以,综合整个缓存机制以及我们的目标---内存优化.我们可以作如下优化:

  • 1.如果图片大小可知,并且都比较小,那么可以设置 hasStableIds 为 true 来优化整个复用效率
  • 2.如果图片比较大,或者大小不可知,那么我们可以在 onViewRecycled 函数中释放图片内存.但是 hasStableIds 肯定不能是 true 了.

参考文章:https://www.jianshu.com/p/efe81969f69d
这个博主写了关于RecyclerView的一系列文章,都可以阅读阅读加深理解

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