在什么情况下 RecyclerView 的缓存机制会失效?即本该被回收的表项没能回收,无法回收就无法复用,这对列表的性能会有多大影响?从一个实例出发,探究下答案。
这篇 Demo 效果如下:
列表表项是一个 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
来触发表项动画,其中的animSet
和animObject
把构建动画的代码做了一层 DSL 的封装,以增加可读性。(对源码感兴趣可以点击这里)
运行 Demo,滑动列表,发现这次只回调了onViewRecycled()
,说明表项成功地被回收了。
并不是所有的表项动画都会阻止表项被回收,只有通过
ViewPropertyAnimator
做动画才会。
顺手又做了一个实验,纵向 RecyclerView 中嵌套横向 RecyclerView,并且横向 RecyclerView 自动滚动:
滚动是通过每隔一段时间调用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 系列文章目录如下: