RecyclerView 性能优化 | 是什么在破坏缓存机制?

简介: 在什么情况下 RecyclerView 的缓存机制会失效?即本该被回收的表项没能回收,无法回收就无法复用,这对列表的性能会有多大影响?从一个实例出发,探究下答案。 列表表项是一个 TextView,它

在什么情况下 RecyclerView 的缓存机制会失效?即本该被回收的表项没能回收,无法回收就无法复用,这对列表的性能会有多大影响?从一个实例出发,探究下答案。

这篇 Demo 效果如下:

1617076611961.gif

列表表项是一个 TextView,它在做水平位移动画。

这种场景下,当表项滑出屏幕后会被回收吗?

监听表项回收

RecyclerView.Adapter提供了两个监听表项回收状态的回调:

public class RecyclerView
    public abstract static class Adapter<VH extends ViewHolder> {
        // 表项回收失败
        public boolean onFailedToRecycleView(@NonNull VH holder) {
            return false;
        }
        // 表项回收成功
        public void onViewRecycled(@NonNull VH holder) {
        }
    }
}

在自定义 Adapter 中重载这两个方法,再把事件通过 lambda 传递出去:

class VarietyAdapter : RecyclerView.Adapter<ViewHolder>() {
    // lambda
    var onFailedToRecycleView: ((holder: ViewHolder) -> Boolean)? = null
    var onViewRecycled: ((holder: ViewHolder) -> Unit)? = null
    
    // 重写
    override fun onFailedToRecycleView(holder: ViewHolder): Boolean {
        return onFailedToRecycleView?.invoke(holder) ?: return super.onFailedToRecycleView(holder)
    }
    // 重写
    override fun onViewRecycled(holder: ViewHolder) {
        onViewRecycled?.invoke(holder)
    }
}

然后就可以在业务层监听表项回收状态:

val adapter = VarietyAdapter()
adapter.onViewRecycled = { holder ->
    Log.v("test", "view of type=${holder.itemViewType} is recycled")
}
adapter.onFailedToRecycleView = { holder ->
    Log.v("test", "view of type=${holder.itemViewType} failed in recycled")
    false
}

运行 Demo,滑动列表,发现只有onFailedToRecycleView()被回调了。即 Demo 场景下,滑出屏幕的表项回收失败。

有条件地回收表项

为啥表项做了动画就不能被回收?

public class RecyclerView {
    public final class Recycler {
        // 回收表项
        void recycleViewHolderInternal(ViewHolder holder) {
            // 获取 ViewHolder transient 状态
            final boolean transientStatePreventsRecycling = holder.doesTransientStatePreventRecycling();
            // 是否强制回收
            final boolean forceRecycle = mAdapter != null
                    && transientStatePreventsRecycling // transient 状态阻止回收
                    && mAdapter.onFailedToRecycleView(holder); // 回调回收失败
            // 强制回收 或者 可以被回收
            if (forceRecycle || holder.isRecyclable()) {
                ...
                // 回收表项
                addViewHolderToRecycledViewPool(holder, true);
                ...
            } else {
                // 回收表项失败
                ...
            }
            ...
        }
    }
}

回收表项是有条件的,要么强制回收forceRecycle,要么 ViewHolder 可被回收holder.isRecyclable(),当两个条件都不满足时,表项就不会被回收。(更详细的 RecyclerView 回收分析可以点击RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?

  • forceRecycle的值由三个条件决定:
final boolean forceRecycle = mAdapter != null
    && transientStatePreventsRecycling // transient 状态阻止回收
    && mAdapter.onFailedToRecycleView(holder); // 回调回收失败

其中transientStatePreventsRecycling表示 itemView 的状态是否为 transient,若是,则表项不能被回收。

final boolean transientStatePreventsRecycling = holder.doesTransientStatePreventRecycling();

public class RecyclerView {
    public abstract static class ViewHolder {
        boolean doesTransientStatePreventRecycling() {
            return (mFlags & FLAG_NOT_RECYCLABLE) == 0 
                && ViewCompat.hasTransientState(itemView);
        }
    }
}

public class ViewCompat {
    public static boolean hasTransientState(@NonNull View view) {
        if (Build.VERSION.SDK_INT >= 16) {
            return view.hasTransientState();
        }
        return false;
    }
}

public class View
    public boolean hasTransientState() {
        return (mPrivateFlags2 & PFLAG2_HAS_TRANSIENT_STATE) == PFLAG2_HAS_TRANSIENT_STATE;
    }
}

经过一系列调用链,最终会调用View.hasTransientState判断 View 是否具有PFLAG2_HAS_TRANSIENT_STATE标志位。

若 View 处于 transient 状态,则 transientStatePreventsRecycling 为 true,导致forceRecycle的第三个条件表达式mAdapter.onFailedToRecycleView(holder)会被执行,表示回收表项到缓存池失败。

  • holder.isRecyclable()的值依然由表项的 transient 状态决定
public class RecyclerView
    public abstract static class ViewHolder {
        // 判断 ViewHolder 是否可被回收
        public final boolean isRecyclable() {
            return (mFlags & FLAG_NOT_RECYCLABLE) == 0
                    // 如果 itemView 处于 transient 状态, 则表项不能被回收
                    && !ViewCompat.hasTransientState(itemView);
        }
    }
}

至此可以得出结论:

若 itemView 处于 transient 状态,则对应的 ViewHolder 不会被回收到 RecycledViewPool

什么情况下 View 会被置为 transient 状态?

全局搜索下View.setHasTransientState(true)调用的地方:

public class ViewPropertyAnimator {
    private void startAnimation() {
        mView.setHasTransientState(true);// 设置 View 为 transient 状态
        ValueAnimator animator = ValueAnimator.ofFloat(1.0f);
        ...
        animator.start();
    }
}

当通过ViewPropertyAnimator启动动画的时候,会将 View 置为 transient 状态。全局仅此一处。

刚才我是这样触发表项动画的:

    override fun onBindViewHolder(holder: TextViewHolder2, data: String, index: Int, action: ((Any?) -> Unit)?) {
        holder.tv?.let { tv ->
            tv.text = data
            ViewCompat.animate(tv).translationX(900f).setDuration(10000).start()
        }
    }

其中的ViewCompat.animate()会返回一个ViewPropertyAnimatorCompat对象:

public final class ViewPropertyAnimatorCompat {
    public static ViewPropertyAnimatorCompat animate(@NonNull View view) {
        if (sViewPropertyAnimatorMap == null) {
            sViewPropertyAnimatorMap = new WeakHashMap<>();
        }
        ViewPropertyAnimatorCompat vpa = sViewPropertyAnimatorMap.get(view);
        if (vpa == null) {
            vpa = new ViewPropertyAnimatorCompat(view);
            sViewPropertyAnimatorMap.put(view, vpa);
        }
        return vpa;
    }
}

它最终会通过ViewPropertyAnimator来触发动画。所以 itemView 会被置为 transient 状态。

那使用其他方式触发的动画会阻碍表项被回收吗?

把做动画的代码改动如下:

    override fun onBindViewHolder(holder: TextViewHolder2, data: String, index: Int, action: ((Any?) -> Unit)?) {
        holder.tv?.let { tv ->
            tv.text = data
            animSet {
                animObject {
                    target = tv
                    translationX = floatArrayOf(0f, 900f)
                }
                duration = 10000L
            }.start()
        }
    }

运用了ObjecAnimator来触发表项动画,其中的animSetanimObject把构建动画的代码做了一层 DSL 的封装,以增加可读性。(对源码感兴趣可以点击这里

运行 Demo,滑动列表,发现这次只回调了onViewRecycled(),说明表项成功地被回收了。

并不是所有的表项动画都会阻止表项被回收,只有通过 ViewPropertyAnimator做动画才会。

顺手又做了一个实验,纵向 RecyclerView 中嵌套横向 RecyclerView,并且横向 RecyclerView 自动滚动:

1617095909645.gif

滚动是通过每隔一段时间调用RecyclerView.smoothScrollBy(x, y)实现的。

这种场景,表项会被正常回收。

不回收,不复用的后果

缓存池RecycledViewPool中缓存的是 ViewHolder 实例,这样做的好处是,当需要构建同类型的 ViewHolder 时不用重新执行onCreateViewHolder(),从缓存池中直接获取即可,这样就缩短了构建表项的耗时,所以缓存池是用空间换时间的一种机制。(关于 RecyclerView 多级缓存的详细讲解可以点击RecyclerView 缓存机制 | 如何复用表项?

打开 AndroidStudio 的 Profile,分别对比了表项能回收和不能回收,这两种情况下列表的内存性能。

若表项正常回收,即也能被正常复用,在列表滑动时,内存中不会有超过 14 个 ViewHolder 对象(显示在屏幕中的 9 个 + 缓存池中 5 个 ViewHolder)

若表项不能被回收,即表项也无法被复用,则在滑动列表过程中会不断地重新创新 ViewHolder 对象,并且所有这些 ViewHolder 都不会被 JVM 回收掉,一直存在于内存中。这样内存就会随着滑动而不断的往上涨,都不带掉头的。

可见 RecyclerView 的缓存池不仅是一种空间换时间的机制,也是一种内存优化的手段

让做动画的表项也被回收

即使通过ViewPropertyAnimator做动画,也有办法让表项被正常回收:

val adapter = VarietyAdapter()
adapter.onFailedToRecycleView = { holder ->
    // 在回收表项之前取消动画
    (holder as? TextViewHolder)?.tv?.animate()?.cancel()
    // 返回 true 表示强制回收表项
    true
}

只要在onFailedToRecycleView()回调中取消动画,并且返回 true 表示强制回收即可。

推荐阅读

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 的滚动是怎么实现的?| 解锁阅读源码新姿势]()
目录
相关文章
|
7天前
|
缓存 Java 数据库连接
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
文章介绍了MyBatis的缓存机制,包括一级缓存和二级缓存的配置和使用,以及如何整合第三方缓存EHCache。详细解释了一级缓存的生命周期、二级缓存的开启条件和配置属性,以及如何通过ehcache.xml配置文件和logback.xml日志配置文件来实现EHCache的整合。
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
|
2月前
|
缓存 应用服务中间件 nginx
Web服务器的缓存机制与内容分发网络(CDN)
【8月更文第28天】随着互联网应用的发展,用户对网站响应速度的要求越来越高。为了提升用户体验,Web服务器通常会采用多种技术手段来优化页面加载速度,其中最重要的两种技术就是缓存机制和内容分发网络(CDN)。本文将深入探讨这两种技术的工作原理及其实现方法,并通过具体的代码示例加以说明。
87 1
|
14天前
|
存储 缓存 Android开发
Android RecyclerView 缓存机制深度解析与面试题
本文首发于公众号“AntDream”,详细解析了 `RecyclerView` 的缓存机制,包括多级缓存的原理与流程,并提供了常见面试题及答案。通过本文,你将深入了解 `RecyclerView` 的高性能秘诀,提升列表和网格的开发技能。
38 8
|
8天前
|
缓存 Java Python
python垃圾回收&缓存机制
python垃圾回收&缓存机制
|
2月前
|
存储 缓存 JavaScript
深入理解后端开发中的缓存机制
【8月更文挑战第31天】本文将通过一个实际的后端开发案例,介绍如何有效地使用缓存来提高应用性能。我们将从基础概念开始,逐步深入到缓存策略的实施,最后通过代码示例展示如何在Node.js环境中实现一个简单的缓存系统。无论你是缓存新手还是希望优化现有系统的开发者,这篇文章都将为你提供实用的指导和启示。
|
2月前
|
存储 缓存 关系型数据库
Django后端架构开发:缓存机制,接口缓存、文件缓存、数据库缓存与Memcached缓存
Django后端架构开发:缓存机制,接口缓存、文件缓存、数据库缓存与Memcached缓存
35 0
|
3月前
|
存储 缓存 前端开发
(三)Nginx一网打尽:动静分离、压缩、缓存、黑白名单、跨域、高可用、性能优化...想要的这都有!
早期的业务都是基于单体节点部署,由于前期访问流量不大,因此单体结构也可满足需求,但随着业务增长,流量也越来越大,那么最终单台服务器受到的访问压力也会逐步增高。时间一长,单台服务器性能无法跟上业务增长,就会造成线上频繁宕机的现象发生,最终导致系统瘫痪无法继续处理用户的请求。
|
19天前
|
canal 缓存 NoSQL
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
根据对一致性的要求程度,提出多种解决方案:同步删除、同步删除+可靠消息、延时双删、异步监听+可靠消息、多重保障方案
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
|
2月前
|
缓存 NoSQL Java
Redis深度解析:解锁高性能缓存的终极武器,让你的应用飞起来
【8月更文挑战第29天】本文从基本概念入手,通过实战示例、原理解析和高级使用技巧,全面讲解Redis这一高性能键值对数据库。Redis基于内存存储,支持多种数据结构,如字符串、列表和哈希表等,常用于数据库、缓存及消息队列。文中详细介绍了如何在Spring Boot项目中集成Redis,并展示了其工作原理、缓存实现方法及高级特性,如事务、发布/订阅、Lua脚本和集群等,帮助读者从入门到精通Redis,大幅提升应用性能与可扩展性。
60 0
|
20天前
|
存储 NoSQL Redis
SpringCloud基础7——Redis分布式缓存,RDB,AOF持久化+主从+哨兵+分片集群
Redis持久化、RDB和AOF方案、Redis主从集群、哨兵、分片集群、散列插槽、自动手动故障转移
SpringCloud基础7——Redis分布式缓存,RDB,AOF持久化+主从+哨兵+分片集群
下一篇
无影云桌面