RecyclerView 缓存机制 | 回收到哪去?

简介: ---# 主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy# 贡献主题:https://github.com/xitu/juejin-markdown-themestheme: githubhighlight: github---RecyclerView 内存性能优越,这得益于它独特的缓存机制,上两篇已经分析了 RecyclerView 缓存机制会回收哪些表项,及如何从缓存中获取表项。本篇在此基础上继续走读源码,分析“回收的表项是以怎样的形式存放”。这是`RecyclerView`缓存机制系列文章的第三

RecyclerView 内存性能优越,这得益于它独特的缓存机制,上两篇已经分析了 RecyclerView 缓存机制会回收哪些表项,及如何从缓存中获取表项。本篇在此基础上继续走读源码,分析“回收的表项是以怎样的形式存放”。

这是RecyclerView缓存机制系列文章的第三篇,系列文章的目录如下:

  1. RecyclerView 缓存机制 | 如何复用表项?
  2. RecyclerView 缓存机制 | 回收些什么?
  3. RecyclerView 缓存机制 | 回收到哪去?
  4. RecyclerView缓存机制 | scrap view 的生命周期
  5. RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系
  6. RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?

如果想直接看结论可以移步到第四篇末尾(你会后悔的,过程更加精彩)。

回收入口

上一篇以列表滑动事件为起点沿着调用链一直往下寻找,验证了“滑出屏幕的表项”会被回收。那它们被回收去哪里了?沿着上一篇的调用链继续往下探究:

public class LinearLayoutManager {
    ...
    // 回收滚出屏幕的表项
    private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
        final int limit = dt;
        final int childCount = getChildCount();
            //遍历LinearLayoutManager的孩子找出其中应该被回收的
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                //直到表项底部纵坐标大于 limit 隐形线,回收该表项以上的所有表项
                if (mOrientationHelper.getDecoratedEnd(child) > limit
                        || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                    //回收索引为 0 到 i-1 的表项
                    recycleChildren(recycler, 0, i);
                    return;
                }
            }
    }
    ...
}

limit 隐形线 是“列表滚动后,哪些表项被该被回收”的依据,当列表向下滚动时,所有位于这条线上方的表项都会被回收。关于 limit隐形线 的详细解释可以点击这里

recycleViewsFromStart()通过遍历找到滑出屏幕的表项,然后调用了recycleChildren()回收他们:

public class LinearLayoutManager {
    // 回收子表项
    private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
        if (startIndex == endIndex) {
            return;
        }
        if (endIndex > startIndex) {
            for (int i = endIndex - 1; i >= startIndex; i--) {
                removeAndRecycleViewAt(i, recycler);
            }
        } else {
            for (int i = startIndex; i > endIndex; i--) {
                removeAndRecycleViewAt(i, recycler);
            }
        }
    }
}

最终调用了父类LayoutManager.removeAndRecycleViewAt()

public abstract static class LayoutManager {
        public void removeAndRecycleViewAt(int index, Recycler recycler) {
            final View view = getChildAt(index);
            removeViewAt(index);
            recycler.recycleView(view);
        }
}

先从LayoutManager中删除表项,然后调用Recycler.recycleView()回收表项:

public final class Recycler {
        public void recycleView(View view) {
            // 获取表项 ViewHolder
            ViewHolder holder = getChildViewHolderInt(view);
            if (holder.isTmpDetached()) {
                removeDetachedView(view, false);
            }
            if (holder.isScrap()) {
                holder.unScrap();
            } else if (holder.wasReturnedFromScrap()) {
                holder.clearReturnedFromScrapFlag();
            }
            recycleViewHolderInternal(holder);
        }
}

先通过表项视图拿到了对应ViewHolder,然后把其传入Recycler.recycleViewHolderInternal(),现在就可以更准地回答上一篇的那个问题“回收些啥?”:回收的是滑出屏幕表项对应的ViewHolder

public final class Recycler {
        ...
        int mViewCacheMax = DEFAULT_CACHE_SIZE;
        static final int DEFAULT_CACHE_SIZE = 2;
        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
        ...

        void recycleViewHolderInternal(ViewHolder holder) {
            ...
            if (forceRecycle || holder.isRecyclable()) {
                //先存在mCachedViews里面
                //这里的判断条件决定了复用mViewCacheMax中的ViewHolder时不需要重新绑定数据
                if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                        | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE
                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                    // Retire oldest cached view
                    //如果mCachedViews大小超限了,则删掉最老的被缓存的ViewHolder
                    int cachedViewSize = mCachedViews.size();
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize;
                    if (ALLOW_THREAD_GAP_WORK
                            && cachedViewSize > 0
                            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                        // when adding the view, skip past most recently prefetched views
                        int cacheIndex = cachedViewSize - 1;
                        while (cacheIndex >= 0) {
                            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                            if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                                break;
                            }
                            cacheIndex--;
                        }
                        targetCacheIndex = cacheIndex + 1;
                    }
                    //ViewHolder加到缓存中
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                //若ViewHolder没有入缓存则存入回收池
                if (!cached) {
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } else {
                ...
            }
            ...
}

ViewHolder 最终的落脚点有两个:

  1. mCachedViews
  2. RecycledViewPool

落脚点通过cached这个布尔值,实现互斥,即ViewHolder要么存入mCachedViews,要么存入pool

mCachedViews有大小限制,默认只能存2个ViewHolder,当第三个ViewHolder存入时会把第一个移除掉:

public final class Recycler {
        // 讲 mCachedViews 中的 ViewHolder 移到 RecycledViewPool 中
        void recycleCachedViewAt(int cachedViewIndex) {
            ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
            //将ViewHolder加入到回收池
            addViewHolderToRecycledViewPool(viewHolder, true);
            //将ViewHolder从cache中移除
            mCachedViews.remove(cachedViewIndex);
        }
        ...
}

mCachedViews移除掉的ViewHolder会加入到回收池中。 mCachedViews有点像“回收池预备队列”,即总是先回收到mCachedViews,当它放不下的时候,按照先进先出原则将最先进入的ViewHolder存入回收池

public final class Recycler {
        // 缓存池实例
        RecycledViewPool mRecyclerPool;
        // 将viewHolder存入缓存池
        void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
            ...
            getRecycledViewPool().putRecycledView(holder);
        }
        // 获取 RecycledViewPool 实例
        RecycledViewPool getRecycledViewPool() {
            if (mRecyclerPool == null) {
                mRecyclerPool = new RecycledViewPool();
            }
            return mRecyclerPool;
        }
}

//缓存池
public static class RecycledViewPool {
        // 但类型 ViewHolder 列表
        static class ScrapData {
            // 最终存储 ViewHolder 实例的列表
            ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            //每种类型的 ViewHolder 最多存 5 个
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            ...
        }
        //键值对:以 viewType 为键,ScrapData 为值,用以存储不同类型的 ViewHolder 列表
        SparseArray<ScrapData> mScrap = new SparseArray<>();
        //ViewHolder 入池 按 viewType 分类入池,相同的 ViewType 存放在同一个列表中
        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;
            }
            // 入回收池之前重置 ViewHolder
            scrap.resetInternal();
            // 最终 ViewHolder 入池
            scrapHeap.add(scrap);
        }
}

ViewHolder会按viewType分类存入回收池,最终存储在ScrapData ArrayList中,回收池数据结构分析详见RecyclerView缓存机制(咋复用?)

缓存优先级

还记得RecyclerView缓存机制(咋复用?)中得出的结论吗?这里再引用一下:

虽然为了获取ViewHolder做了5次尝试(共从6个地方获取),先排除3种特殊情况,即从mChangedScrap获取、通过id获取、从自定义缓存获取,正常流程中只剩下3种获取方式,优先级从高到低依次是:

  1. 从 mAttachedScrap 获取
  2. 从 mCachedViews 获取
  3. 从 mRecyclerPool 获取
    这样的缓存优先级意味着,对应的复用性能也是从高到低(复用性能越好意味着所做的昂贵操作越少)
  4. 最坏情况:重新创建 ViewHodler 并重新绑定数据
  5. 次好情况:复用 ViewHolder 但重新绑定数据
  6. 最好情况:复用 ViewHolder 且不重新绑定数据

当时分析了mAttachedScrapmRecyclerPool的复用性能,即 mRecyclerPool中复用的ViewHolder需要重新绑定数据,从mAttachedScrap 中复用的ViewHolder不需要重新创建也不需要重新绑定数据

把存入mCachedViews的代码和复用时绑定数据的代码结合起来看一下:

void recycleViewHolderInternal(ViewHolder holder) {
    ...
    //满足这个条件才能存入mCachedViews
    if (mViewCacheMax > 0
        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
        | ViewHolder.FLAG_REMOVED
        | ViewHolder.FLAG_UPDATE
        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
    }
    ...
}

ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
    ...
    //满足这个条件就需要重新绑定数据
    if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()){
    }
    ...

重新绑定数据的三个条件中,holder.needsUpdate()holder.isInvalid()都是false时才能存入mCachedViews ,而!holder.isBound()对于mCachedViews 中的ViewHolder来说必然为false,因为只有当调用ViewHolder.resetInternal()重置ViewHolder后,才会将其设置为未绑定状态,而只有存入回收池时才会重置ViewHolder。所以 mCachedViews中复用的ViewHolder不需要重新绑定数据

总结

  • 滑出屏幕表项对应的 ViewHolder 会被回收到mCachedViews+mRecyclerPool 结构中。
  • mCachedViews是 ArrayList ,默认存储最多2个 ViewHolder ,当它放不下的时候,按照先进先出原则将最先进入的 ViewHolder 存入回收池的方式来腾出空间。mRecyclerPool 是 SparseArray ,它会按viewType分类存储 ViewHolder ,默认每种类型最多存5个。
  • mRecyclerPool中复用的 ViewHolder 需要重新绑定数据
  • mCachedViews中复用的 ViewHolder 不需要重新绑定数据

推荐阅读

RecyclerView 系列文章目录如下:

  1. RecyclerView 缓存机制 | 如何复用表项?
  2. RecyclerView 缓存机制 | 回收些什么?
  3. RecyclerView 缓存机制 | 回收到哪去?
  4. RecyclerView缓存机制 | scrap view 的生命周期
  5. 读源码长知识 | 更好的RecyclerView点击监听器
  6. 代理模式应用 | 每当为 RecyclerView 新增类型时就很抓狂
  7. 更好的 RecyclerView 表项子控件点击监听器
  8. 更高效地刷新 RecyclerView | DiffUtil二次封装
  9. 换一个思路,超简单的RecyclerView预加载
  10. RecyclerView 动画原理 | 换个姿势看源码(pre-layout)
  11. RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系
  12. RecyclerView 动画原理 | 如何存储并应用动画属性值?
  13. RecyclerView 面试题 | 列表滚动时,表项是如何被填充或回收的?
  14. RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?
  15. RecyclerView 性能优化 | 把加载表项耗时减半 (一)
  16. RecyclerView 性能优化 | 把加载表项耗时减半 (二)
  17. RecyclerView 性能优化 | 把加载表项耗时减半 (三)
  18. RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势
  19. RecyclerView 的滚动时怎么实现的?(二)| Fling
  20. RecyclerView 刷新列表数据的 notifyDataSetChanged() 为什么是昂贵的?
目录
相关文章
|
2月前
|
缓存 Java 数据库连接
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
文章介绍了MyBatis的缓存机制,包括一级缓存和二级缓存的配置和使用,以及如何整合第三方缓存EHCache。详细解释了一级缓存的生命周期、二级缓存的开启条件和配置属性,以及如何通过ehcache.xml配置文件和logback.xml日志配置文件来实现EHCache的整合。
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
|
3月前
|
缓存 应用服务中间件 nginx
Web服务器的缓存机制与内容分发网络(CDN)
【8月更文第28天】随着互联网应用的发展,用户对网站响应速度的要求越来越高。为了提升用户体验,Web服务器通常会采用多种技术手段来优化页面加载速度,其中最重要的两种技术就是缓存机制和内容分发网络(CDN)。本文将深入探讨这两种技术的工作原理及其实现方法,并通过具体的代码示例加以说明。
337 1
|
1月前
|
存储 缓存 负载均衡
Nginx代理缓存机制
【10月更文挑战第2天】
69 4
|
1月前
|
存储 缓存 NoSQL
深入理解后端缓存机制的重要性与实践
本文将探讨在后端开发中缓存机制的应用及其重要性。缓存,作为提高系统性能和用户体验的关键技术,对于后端开发来说至关重要。通过减少数据库访问次数和缩短响应时间,缓存可以显著提升应用程序的性能。本文将从缓存的基本概念入手,介绍常见的缓存策略和实现方式,并通过实例展示如何在后端开发中有效应用缓存技术。最后,我们将讨论缓存带来的一些挑战及其解决方案,帮助您在实际项目中更好地利用缓存机制。
|
2月前
|
存储 缓存 Android开发
Android RecyclerView 缓存机制深度解析与面试题
本文首发于公众号“AntDream”,详细解析了 `RecyclerView` 的缓存机制,包括多级缓存的原理与流程,并提供了常见面试题及答案。通过本文,你将深入了解 `RecyclerView` 的高性能秘诀,提升列表和网格的开发技能。
67 8
|
2月前
|
缓存 Java Python
python垃圾回收&缓存机制
python垃圾回收&缓存机制
|
3月前
|
存储 缓存 JavaScript
深入理解后端开发中的缓存机制
【8月更文挑战第31天】本文将通过一个实际的后端开发案例,介绍如何有效地使用缓存来提高应用性能。我们将从基础概念开始,逐步深入到缓存策略的实施,最后通过代码示例展示如何在Node.js环境中实现一个简单的缓存系统。无论你是缓存新手还是希望优化现有系统的开发者,这篇文章都将为你提供实用的指导和启示。
|
1月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
74 6
|
9天前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
10天前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构