深入理解RecyclerView回收复用机制

955 阅读5分钟

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动


本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

前言

学习源码,研究源码编程思想,是程序开发者进阶的必经之路

大家都知道RecyclerView有回收复用机制,那么回收复用机制是如何作用的?

今天我们就用源码来讲解,一起学习

Android进阶:全面拥抱Activity Results API来替代onActivityResult

Android源码进阶之深入理解SharedPreference原理机制

学习就应该“劳逸结合”

Android进阶之后台任务和定时服务,放弃AlarmManager全面拥抱WorkManager

Android转场动画深度解析

Java进阶之深入理解负载均衡的5种算法实现原理

Android进阶之Coil-为kotlin而生的图片库详解

历史推荐,共勉

Android进阶之view坐标系全面详解 国庆快乐:一起学习进步

一、Recycler介绍

RecyclerView是通过内部类Recycler管理的缓存,那么Recycler中缓存的是什么?我们知道RecyclerView在存在大量数据时依然可以滑动的如丝滑般顺畅,而RecyclerView本身是一个ViewGroup,那么滑动时避免不了添加或移除子View(子View通过RecyclerView#Adapter中的onCreateViewHolder创建),如果每次使用子View都要去重新创建,肯定会影响滑动的流 畅性,所以RecyclerView通过Recycler来缓存的是ViewHolder(内部包含子View),这样在滑动时可以复用子View,某些条件下还可以复用子View绑定的数据。所以本质上缓存是为了减少重复绘制View和绑定数据的时间,从而提高了滑动时的性能

public final class Recycler {

final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();

ArrayList<ViewHolder> mChangedScrap = null;

final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

private final List<ViewHolder>

mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;

int mViewCacheMax = DEFAULT_CACHE_SIZE;

RecycledViewPool mRecyclerPool;

private ViewCacheExtension mViewCacheExtension;

static final int DEFAULT_CACHE_SIZE = 2;

Recycler缓存ViewHolder对象有4个等级,优先级从高到底依次为:

1、ArrayList mAttachedScrap --- 缓存屏幕中可见范围的ViewHolder

2、ArrayList mCachedViews ---- 缓存滑动时即将与RecyclerView分离的ViewHolder,按子View的position或id缓存,默认最多存放2个

3、ViewCacheExtension mViewCacheExtension --- 开发者自行实现的缓存

4、RecycledViewPool mRecyclerPool --- ViewHolder缓存池,本质上是一个SparseArray,其中key是ViewType(int类型),value存放的是 ArrayList< ViewHolder>,默认每个ArrayList中最多存放5个ViewHolder。

二、缓存机制分析详解

RecyclerView滑动时会触发onTouchEvent#onMove,回收及复用ViewHolder在这里就会开始。我们知道设置RecyclerView时需要设置LayoutManager,LayoutManager负责RecyclerView的布局,包含对ItemView的获取与复用。以LinearLayoutManager为例,当RecyclerView重新布局时会依次执行下面几个方法:

onLayoutChildren():对RecyclerView进行布局的入口方法

fill(): 负责对剩余空间不断地填充,调用的方法是layoutChunk()

layoutChunk():负责填充View,该View最终是通过在缓存类Recycler中找到合适的View的

上述的整个调用链:onLayoutChildren()->fill()->layoutChunk()->next()->getViewForPosition(),getViewForPosition()即是是从RecyclerView的回收机制实现类Recycler中获取合适的View,

下面主要就来从看这个Recycler#getViewForPosition()的实现;

@NonNull

public View getViewForPosition(int position) {

return getViewForPosition(position, false);

}

View getViewForPosition(int position, boolean dryRun) {

return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;

}

他们都会执行tryGetViewHolderForPositionByDeadline函数,继续跟进去:

//根据传入的position获取ViewHolder

ViewHolder tryGetViewHolderForPositionByDeadline(int position,

boolean dryRun, long deadlineNs) {

...省略

boolean fromScrapOrHiddenOrCache = false;

ViewHolder holder = null;

//预布局 属于特殊情况 从mChangedScrap中获取ViewHolder

if (mState.isPreLayout()) {

holder = getChangedScrapViewForPosition(position);

fromScrapOrHiddenOrCache = holder != null;

}

if (holder == null) {

//1、尝试从mAttachedScrap中获取ViewHolder,此时获取的是屏幕中可见范围中的ViewHolder

//2、mAttachedScrap缓存中没有的话,继续从mCachedViews尝试获取ViewHolder

holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);

...省略

}

if (holder == null) {

final int offsetPosition = mAdapterHelper.findPositionOffset(position);

...省略

final int type = mAdapter.getItemViewType(offsetPosition);

//如果Adapter中声明了Id,尝试从id中获取,这里不属于缓存

if (mAdapter.hasStableIds()) {

holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),

type, dryRun);

}

if (holder == null && mViewCacheExtension != null) {

3、从自定义缓存mViewCacheExtension中尝试获取ViewHolder,该缓存需要开发者实现

final View view = mViewCacheExtension

.getViewForPositionAndType(this, position, type);

if (view != null) {

holder = getChildViewHolder(view);

}

}

if (holder == null) { // fallback to pool

//4、从缓存池mRecyclerPool中尝试获取ViewHolder

holder = getRecycledViewPool().getRecycledView(type);

if (holder != null) {

//如果获取成功,会重置ViewHolder状态,所以需要重新执行Adapter#onBindViewHolder绑定数据

holder.resetInternal();

if (FORCE_INVALIDATE_DISPLAY_LIST) {

invalidateDisplayListInt(holder);

}

}

}

if (holder == null) {

...省略

//5、若以上缓存中都没有找到对应的ViewHolder,最终会调用Adapter中的onCreateViewHolder创建一个

holder = mAdapter.createViewHolder(RecyclerView.this, type);

}

}

boolean bound = false;

if (mState.isPreLayout() && holder.isBound()) {

holder.mPreLayoutPosition = position;

} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {

final int offsetPosition = mAdapterHelper.findPositionOffset(position);

//6、如果需要绑定数据,会调用Adapter#onBindViewHolder来绑定数据

bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);

}

...省略

return holder;

}

image.png

通过mAttachedScrap、mCachedViews及mViewCacheExtension获取的ViewHolder不需要重新创建布局及绑定数据;通过缓存池mRecyclerPool获取的ViewHolder不需要重新创建布局,但是需要重新绑定数据;如果上述缓存中都没有获取到目标ViewHolder,那么就会回调Adapter#onCreateViewHolder创建布局,以及回调Adapter#onBindViewHolder来绑定数据

总结

RecyclerView 滑动场景下的回收复用涉及到的结构体两个:

  • mCachedViews 和 RecyclerViewPool
  • mCachedViews 优先级高于 RecyclerViewPool,回收时,最新的 ViewHolder 都是往 mCachedViews 里放,如果它满了,那就移出一个扔到 ViewPool 里好空出位置来缓存最新的 ViewHolder。
  • 复用时,也是先到 mCachedViews 里找 ViewHolder,但需要各种匹配条件,概括一下就是只有原来位置的卡位可以复用存在 mCachedViews 里的 ViewHolder,如果 mCachedViews 里没有,那么才去 ViewPool 里找。
  • 在 ViewPool 里的 ViewHolder 都是跟全新的 ViewHolder 一样,只要 type 一样,有找到,就可以拿出来复用,重新绑定下数据即可。

\

\