谈谈RecyclerView中的缓存
Android深入理解RecyclerView的缓存机制
RecyclerView在项目中的使用已经很普遍了,可以说是项目中最高频使用的一个控件了。除了布局灵活性、丰富的动画,RecyclerView还有优秀的缓存机制,本文尝试通过源码深入了解一下RecyclerView中的缓存机制。
1. RecyclerView缓存机制与性能优化关系
RecyclerView做性能优化要说复杂也复杂,比如说布局优化,缓存,预加载等等。其优化的点很多,在这些看似独立的点之间,其实存在一个枢纽:Adapter。因为所有的ViewHolder的创建和内容的绑定都需要经过Adaper的两个函数onCreateViewHolder和onBindViewHolder。
因此我们性能优化的本质就是要减少这两个函数的调用时间和调用的次数。如果我们想对RecyclerView做性能优化,必须清楚的了解到我们的每一步操作背后,onCreateViewHolder和onBindViewHolder调用了多少次。因此,了解RecyclerView的缓存机制是RecyclerView性能优化的基础。
为了理解缓存的应用场景,本文首先会简单介绍一下RecyclerView的绘制原理,然后再分析其缓存实现原理。
image2. 绘制原理简述
- RecyclerView.requestLayout开始发生绘制,忽略Measure的过程
- 在Layout的过程会通过LayoutManager.fill去将RecyclerView填满
- LayoutManager.fill会调用LayoutManager.layoutChunk去生成一个具体的ViewHolder
- 然后LayoutManager就会调用Recycler.getViewForPosition向Recycler去要ViewHolder
- Recycler首先去一级缓存(Cache)里面查找是否命中,如果命中直接返回。如果一级缓存没有找到,则去三级缓存查找,如果三级缓存找到了则调用Adapter.bindViewHolder来绑定内容,然后返回。如果三级缓存没有找到,那么就通过Adapter.createViewHolder创建一个ViewHolder,然后调用Adapter.bindViewHolder绑定其内容,然后返回为Recycler。
- 一直重复步骤3-5,知道创建的ViewHolder填满了整个RecyclerView为止。
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;
}
//根据传入的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;
}
上述逻辑用流程图表示:
3. 缓存机制
3.1 源码简析
RecyclerView在Recyler里面实现ViewHolder的缓存,Recycler里面的实现缓存的主要包含以下5个对象:
- ArrayList mAttachedScrap:该层缓存目的是在调用notfyXxx时未改变的item,以及影响RecyclerView重新绘制的情况。mChangedScrap和mAttachedScrap可以看做是一个层级,都是屏幕上可见itemView,只不过区分了状态(改变和未改变)。
- ArrayList mChangedScrap:该层缓存目的是为了当调用notifyItemChanged(pos),notifyItemRangeChanged(pos,count)后该位置信息发生改变的缓存,一般用于change动画,注意mChangedScrap并不是说存储改变的位置并直接复用,而是在预布局时存储改变的holder,重新创建新holder并绑定数据来充当改变位置的数据刷新,然后根据新老holder执行change动画。动画执行完毕后新的holder会被缓存到mRecyclerPool中。那如何复用notifyItemChanged(pos)改变的holder呢?
- ArrayList mCachedViews:作用在滑动,当滑进屏幕或滑出屏幕,为了避免多次bind,是一个大小为2的List。还有用于保存Prefetch的ViewHoder
- 最大的数量为:mViewCacheMax = mRequestedCacheMax + extraCache(extraCache是由prefetch的时候计算出来的)
- ViewCacheExtension mViewCacheExtension:开发者可自定义的一层缓存,是虚拟类ViewCacheExtension的一个实例,开发者可实现方法getViewForPositionAndType(Recycler recycler, int position, int type)来实现自己的缓存。
- 位置固定
- 内容不变
- 数量有限
- mRecyclerPool ViewHolder缓存池,在有限的mCachedViews中如果存不下ViewHolder时,就会把ViewHolder存入RecyclerViewPool中。
- 按照Type来查找ViewHolder
- 每个Type默认最多缓存5个
public final class Recycler {
final ArrayList mAttachedScrap = new ArrayList<>();
ArrayList mChangedScrap = null;
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;
//mCachedViews的默认大小
static final int DEFAULT_CACHE_SIZE = 2;
3.2 缓存机制图解
RecyclerView在设计的时候讲上述5个缓存对象分为了3级。每次创建ViewHolder的时候,会按照优先级依次查询缓存创建ViewHolder。每次讲ViewHolder缓存到Recycler缓存的时候,也会按照优先级依次缓存进去。三级缓存分别是:
- 一级缓存:返回布局和内容都都有效的ViewHolder
- 按照position或者id进行匹配
- 命中一级缓存无需onCreateViewHolder和onBindViewHolder
- mAttachScrap在adapter.notifyXxx的时候用到
- mCachedView:用来解决滑动抖动的情况,默认值为2
- 二级缓存:返回View
- 按照position和type进行匹配
- 直接返回View
- 需要自己继承ViewCacheExtension实现
- 位置固定,内容不发生改变的情况,比如说Header如果内容固定,就可以使用
- 三级缓存:返回布局有效,内容无效的ViewHolder
- 按照type进行匹配,每个type缓存值默认=5
- layout是有效的,但是内容是无效的
- 多个RecycleView可共享,可用于多个RecyclerView的优化
3.3 实例讲解
imagevoid recycleViewHolderInternal(ViewHolder holder) {
if (forceRecycle || holder.isRecyclable()) {
//只有缓存数量大于0,才会放到mCachedViews里
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
//缓存满了移除第一个,放到缓存池中
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// when adding the view, skip past most recently prefetched views
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
targetCacheIndex = cacheIndex + 1;
}
//加入缓存
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
//放到缓存池中
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
// NOTE: A view can fail to be recycled when it is scrolled off while an animation
// runs. In this case, the item is eventually recycled by
// ItemAnimatorRestoreListener#onAnimationFinished.
// TODO: consider cancelling an animation when an item is removed scrollBy,
// to return it to the pool faster
if (DEBUG) {
Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
+ "re-visit here. We are still removing it from animation lists"
+ exceptionLabel());
}
}
// even if the holder is not removed, we still call this method so that it is removed
// from view holder lists.
mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mBindingAdapter = null;
holder.mOwnerRecyclerView = null;
}
}
4. 实战应用
1.使用自定义ViewCacheExtension
//把缓存池中该类型的size设为0
viewPool.setMaxRecycledViews(CacheUtil.TYPE_SPECIAL, 0)
recyclerView.setViewCacheExtension(object : RecyclerView.ViewCacheExtension() {
override fun getViewForPositionAndType(recycler: RecyclerView.Recycler, position: Int, type: Int): View? {
if (type != CacheUtil.TYPE_SPECIAL) {
return null
}
if (adapter.cacheView.isEmpty()) {
return null
}
return adapter.cacheView[0]
}
})
class RecyclerViewAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val dataList = ArrayList<Int>()
val cacheView = ArrayList<View>()
fun setData(list: List<Int>) {
dataList.addAll(list)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
CacheUtil.TYPE_SPECIAL -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_type_special, parent, false)
cacheView.add(view)
SpecialViewHolder(view)
}
else -> EmptyViewHolder(TextView(parent.context))
}
}
使用自定义ViewCacheExtension后,view离屏后再回来不会走onBindViewHolder()方法。
2.让某个ViewHolder不缓存
holder.setIsRecyclable(false),这样的话每次都会走onCreateViewHolder()和onBindViewHolder()方法
3.缓存优化
1.提前初始化viewHolder,放到缓存池中
viewPool.putRecycledView(adapter.onCreateViewHolder(recyclerView, 1))
2.提前初始化view,在onCreateViewHolder的时候去取view
3.自定义ViewCacheExtension
4.适当的增加cacheSize
4.公用缓存池,比如多个viewPager+fragment场景使用,或者全局单利缓存池,感觉用户不大。
5. 注意事项
1.mChangedScrap什么时候会有值
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
有2中做法有值
第一种
val anim = DefaultItemAnimator()
anim.supportsChangeAnimations = true
recyclerView.itemAnimator = anim
adapter.notifyItemChanged(1)
adapter.notifyItemRangeChanged(1, 3)
第二种
class MyItemAnim : DefaultItemAnimator() {
override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean {
return false
}
override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder, payloads: MutableList<Any>): Boolean {
return false
}
}
adapter.notifyItemChanged(1)
adapter.notifyItemChanged(1,"哈哈")
adapter.notifyItemRangeChanged(1, 3, "哈哈")
2.recyclerView.setItemViewCacheSize(0)代表缓存失效吗?
不会,因为prefetch(GapWorker中的一个方法)之后mViewCacheMax会变成mRequestedCacheMax + extraCache
@Override
public void addPosition(int layoutPosition, int pixelDistance) {
mCount++;
}
void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
mCount = 0;
//layout.mPrefetchMaxCountObserved默认值0
if (mCount > layout.mPrefetchMaxCountObserved) {
layout.mPrefetchMaxCountObserved = mCount;
layout.mPrefetchMaxObservedInInitialPrefetch = nested;
view.mRecycler.updateViewCacheSize();
}
}
public void setViewCacheSize(int viewCount) {
mRequestedCacheMax = viewCount;
updateViewCacheSize();
}
void updateViewCacheSize() {
int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
mViewCacheMax = mRequestedCacheMax + extraCache;
// first, try the views that can be recycled
or (int i = mCachedViews.size() - 1;
i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
recycleCachedViewAt(i);
}
}
有2种方式可以让缓存失效
第一种
recyclerView.setItemViewCacheSize(-1)
第二种
recyclerView.setItemViewCacheSize(0)
layoutManager.isItemPrefetchEnabled = false
设置不缓存后,来回滑动让view进入屏幕离开屏幕,viewHolder的item时会多次走onBindViewHolder()方法。