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

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

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


这篇 Demo 效果如下:


image.png


https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5ab87828f6794e82b99f5b2230b92f5d~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp


列表表项是一个 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 自动滚动:


image.png


https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ec41ff347aae45ae86ed92642e1d755d~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp


滚动是通过每隔一段时间调用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 缓存机制 | 如何复用表项?


  1. RecyclerView 缓存机制 | 回收些什么?


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


  1. RecyclerView缓存机制 | scrap view 的生命周期


  1. 读源码长知识 | 更好的RecyclerView点击监听器


  1. 代理模式应用 | 每当为 RecyclerView 新增类型时就很抓狂


  1. 更好的 RecyclerView 表项子控件点击监听器


  1. 更高效地刷新 RecyclerView | DiffUtil二次封装


  1. 换一个思路,超简单的RecyclerView预加载


  1. RecyclerView 动画原理 | 换个姿势看源码(pre-layout)


  1. RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系


  1. RecyclerView 动画原理 | 如何存储并应用动画属性值?


  1. RecyclerView 面试题 | 列表滚动时,表项是如何被填充或回收的?


  1. RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?


  1. RecyclerView 性能优化 | 把加载表项耗时减半 (一)


  1. RecyclerView 性能优化 | 把加载表项耗时减半 (二)


  1. RecyclerView 性能优化 | 把加载表项耗时减半 (三)


  1. RecyclerView 的滚动是怎么实现的?| 解锁阅读源码新姿势


目录
相关文章
|
4月前
|
缓存 并行计算 监控
vLLM 性能优化实战:批处理、量化与缓存配置方案
本文深入解析vLLM高性能部署实践,揭秘如何通过continuous batching、PagedAttention与前缀缓存提升吞吐;详解批处理、量化、并发参数调优,助力实现高TPS与低延迟平衡,真正发挥vLLM生产级潜力。
1037 0
vLLM 性能优化实战:批处理、量化与缓存配置方案
|
10月前
|
缓存 并行计算 PyTorch
PyTorch CUDA内存管理优化:深度理解GPU资源分配与缓存机制
本文深入探讨了PyTorch中GPU内存管理的核心机制,特别是CUDA缓存分配器的作用与优化策略。文章分析了常见的“CUDA out of memory”问题及其成因,并通过实际案例(如Llama 1B模型训练)展示了内存分配模式。PyTorch的缓存分配器通过内存池化、延迟释放和碎片化优化等技术,显著提升了内存使用效率,减少了系统调用开销。此外,文章还介绍了高级优化方法,包括混合精度训练、梯度检查点技术及自定义内存分配器配置。这些策略有助于开发者在有限硬件资源下实现更高性能的深度学习模型训练与推理。
1973 0
|
8月前
|
缓存 负载均衡 网络协议
电商API接口性能优化技术揭秘:缓存策略与负载均衡详解
电商API接口性能优化是提升系统稳定性和用户体验的关键。本文聚焦缓存策略与负载均衡两大核心,详解其在电商业务中的实践。缓存策略涵盖本地、分布式及CDN缓存,通过全量或部分缓存设计和一致性维护,减少后端压力;负载均衡则利用反向代理、DNS轮询等技术,结合动态调整与冗余部署,提高吞吐量与可用性。文中引用大型及跨境电商平台案例,展示优化效果,强调持续监控与迭代的重要性,为电商企业提供了切实可行的性能优化路径。
|
11月前
|
存储 缓存 NoSQL
Redis缓存设计与性能优化
Redis缓存设计与性能优化涵盖缓存穿透、击穿、雪崩及热点key重建等问题。针对缓存穿透,可采用缓存空对象或布隆过滤器;缓存击穿通过随机设置过期时间避免集中失效;缓存雪崩需确保高可用性并使用限流熔断组件;热点key重建利用互斥锁防止大量线程同时操作。此外,开发规范强调键值设计、命令使用和客户端配置优化,如避免bigkey、合理使用批量操作和连接池管理。系统内核参数如vm.swappiness、vm.overcommit_memory及文件句柄数的优化也至关重要。慢查询日志帮助监控性能瓶颈。
432 9
|
存储 缓存 分布式计算
【赵渝强老师】Spark RDD的缓存机制
Spark RDD通过`persist`或`cache`方法可将计算结果缓存,但并非立即生效,而是在触发action时才缓存到内存中供重用。`cache`方法实际调用了`persist(StorageLevel.MEMORY_ONLY)`。RDD缓存可能因内存不足被删除,建议结合检查点机制保证容错。示例中,读取大文件并多次调用`count`,使用缓存后执行效率显著提升,最后一次计算仅耗时98ms。
360 0
【赵渝强老师】Spark RDD的缓存机制
|
存储 缓存 监控
后端开发中的缓存机制:深度解析与最佳实践####
本文深入探讨了后端开发中不可或缺的一环——缓存机制,旨在为读者提供一份详尽的指南,涵盖缓存的基本原理、常见类型(如内存缓存、磁盘缓存、分布式缓存等)、主流技术选型(Redis、Memcached、Ehcache等),以及在实际项目中如何根据业务需求设计并实施高效的缓存策略。不同于常规摘要的概述性质,本摘要直接点明文章将围绕“深度解析”与“最佳实践”两大核心展开,既适合初学者构建基础认知框架,也为有经验的开发者提供优化建议与实战技巧。 ####
|
缓存 Java 数据库连接
深入探讨:Spring与MyBatis中的连接池与缓存机制
Spring 与 MyBatis 提供了强大的连接池和缓存机制,通过合理配置和使用这些机制,可以显著提升应用的性能和可扩展性。连接池通过复用数据库连接减少了连接创建和销毁的开销,而 MyBatis 的一级缓存和二级缓存则通过缓存查询结果减少了数据库访问次数。在实际应用中,结合具体的业务需求和系统架构,优化连接池和缓存的配置,是提升系统性能的重要手段。
520 4
|
缓存 Java 数据库连接
MyBatis缓存机制
MyBatis提供两级缓存机制:一级缓存(Local Cache)默认开启,作用范围为SqlSession,重复查询时直接从缓存读取;二级缓存(Second Level Cache)需手动开启,作用于Mapper级别,支持跨SqlSession共享数据,减少数据库访问,提升性能。
274 1
|
存储 缓存 监控
|
存储 缓存 负载均衡
Nginx代理缓存机制
【10月更文挑战第2天】
402 4