在什么情况下 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;
}
}