文章目录
- 一、RecyclerView的复用机制
- 1.1 复用机制的核心
- 1.1.1 getChangedScrapViewForPosition(mState.isPreLayout())
- 1.1.2 getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) & getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun)
- 1.1.3 RecyclerView.ViewCacheExtension mViewCacheExtension
- 1.1.4 RecyclerPool
- 1.1.5 创建ViewHolder并绑定数据
- 1.2 滑动过程中的回收
- 二、嵌套RecyclerView的共享缓存池RecycledViewPool
- 三、主动触发内层RecyclerView的回收
- 四、触发内层RecyclerView的回收
- 五、setRecycleChildrenOnDetach
- 六、延伸
项目中使用了嵌套RecyclerView,最近在做性能优化的过程中,发现有内层RecyclerView的onCreateHolder方法一直调用,也就是说没有实现内层RecyclerView的回收复用。
网上大量文章介绍了通过设置RecyclerPool的方式来实现内层复用,但是在应用过程中发现并没有起到作用(后文会分析具体原因),因此详细的再对RecyclerView源码中的复用机制进行一下分析
一、RecyclerView的复用机制
- 最坏情况:重新创建ViewHodler并重新绑定数据
- 次好情况:复用ViewHolder但重新绑定数据
- 最好情况:复用ViewHolder且不重新绑定数据
1.1 复用机制的核心
如果列表中每个移出屏幕的表项都直接销毁,移入时重新创建,很不经济。所以RecyclerView引入了缓存机制,缓存复用的核心函数是:
/**
* Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
* cache, the RecycledViewPool, or creating it directly.
* 尝试获得指定位置的ViewHolder,要么从scrap,cache,RecycledViewPool中获取,要么直接重新创建
* @return ViewHolder for requested position
*/
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
//0 从changed scrap集合中获取ViewHolder
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
//1. 通过position从attach scrap或一级回收缓存中获取ViewHolder
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
...
}
if (holder == null) {
...
final int type = mAdapter.getItemViewType(offsetPosition);
//2. 通过id在attach scrap集合和一级回收缓存中查找viewHolder
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
...
}
//3. 从自定义缓存中获取ViewHolder
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
...
}
//4.从缓存池中拿ViewHolder
if (holder == null) { // fallback to pool
...
holder = getRecycledViewPool().getRecycledView(type);
...
}
//所有缓存都没有命中,只能创建ViewHolder
if (holder == null) {
...
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
}
//只有invalid的viewHolder才能绑定视图数据
else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"
+ " come here only in pre-layout. Holder: " + holder
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//获得ViewHolder后,绑定视图数据
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
...
return holder;
}
其中定义了 针对ViewHolder的四层缓存机制,优先级从高到低分别为:
- ArrayList mAttachedScrap
- ArrayList mCachedViews
- ViewCacheExtension mViewCacheExtension
- RecycledViewPool mRecyclerPool。
- 如果四层缓存都未命中,则重新创建并绑定ViewHolder对象
1.1.1 getChangedScrapViewForPosition(mState.isPreLayout())
只有在mState.isPreLayout()为true时才会做这次尝试
1.1.2 getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) & getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun)
- mAttachedScrap
- 用于布局过程中屏幕可见表项的回收和复用
- 没有大小限制,但最多包含屏幕可见表项
- mChildHelper
- mCachedViews
- mCachedViews中缓存的ViewHolder只能复用于指定位置
- 默认大小限制为2,放不下时,按照先进先出原则将最先进入的ViewHolder存入回收池以腾出空间。
- 用于移出屏幕表项的回收和复用,且只能用于指定位置的表项,有点像“回收池预备队列”,即总是先回收到mCachedViews,当它放不下的时候,按照先进先出原则将最先进入的ViewHolder存入回收池。
1.1.3 RecyclerView.ViewCacheExtension mViewCacheExtension
public abstract static class ViewCacheExtension {
View getViewForPositionAndType(Recycler recycler, int position, int type);
}
ViewCacheExtension提供了额外的表项缓存层,用户帮助开发者自己控制表项缓存
当Recycler从attached scrap和first level cache中未能找到匹配的表项时,它会在去RecycledViewPool中查找之前,先尝试从自定义缓存中查找
1.1.4 RecyclerPool
前四次尝试都未果,最后从RecycledViewPool中获取ViewHolder。 此处并没有严格的检验逻辑
- mRecyclerPool:对ViewHolder按viewType分类存储(通过SparseArray),同类ViewHolder存储在默认大小为5的ArrayList中。
- 用于移出屏幕表项的回收和复用,且只能用于指定viewType的表项
- 从mRecyclerPool中复用的ViewHolder需要重新绑定数据,从mAttachedScrap中复用的ViewHolder不要重新出创建也不需要重新绑定数据。
1.1.5 创建ViewHolder并绑定数据
- 如果依然没有获得ViewHolder,只能重新创建并绑定数据。沿着调用链往下,就会找到熟悉的onCreateViewHolder()和onBindViewHolder()。
- 绑定数据的逻辑嵌套在一个大大的if中(原来并不是每次都要绑定数据,只有满足特定条件时才需要绑定。 )
1.2 滑动过程中的回收
众多回收场景中最显而易见的就是“滚动列表时移出屏幕的表项被回收”, 可以以RecyclerView.onTouchEvent()为切入点寻觅“回收表项”的时机:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
...
@VisibleForTesting LayoutManager mLayout;
...
boolean scrollByInternal(int x, int y, MotionEvent ev) {
...
if (mAdapter != null) {
...
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
...
}
...
}
RecyclerView把滚动交给了LayoutManager来处理,并最终交给LayoutManager.removeAndRecycleViewAt()
public abstract static class LayoutManager {
/**
* Remove a child view and recycle it using the given Recycler.
*
* @param index Index of child to remove and recycle
* @param recycler Recycler to use to recycle child
*/
public void removeAndRecycleViewAt(int index, Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
}
}
最后通过Recycler.recycleView()进行回收。 滑出屏幕表项对应的ViewHolder会被回收到mCachedViews+mRecyclerPool结构中,mCachedViews是ArrayList,默认存储最多2个ViewHolder,当它放不下的时候,按照先进先出原则将最先进入的ViewHolder存入回收池的方式来腾出空间。mRecyclerPool是SparseArray,它会按viewType分类存储ViewHolder,默认每种类型最多存5个。
二、嵌套RecyclerView的共享缓存池RecycledViewPool
我们来看下在网上找到的一张示例图:
当用户滚动横向列表的时候,inner RecyclerView可以流畅的滚动。但是当垂直滚动的时候, inner RecyclerView 中的每个view再次inflate了一遍,从而感觉很卡顿, 这是因为每个嵌套的 RecyclerViews 都有各自的 view pool.
如果多个 RecycledView 的 Adapter 是一样的,比如嵌套的 RecyclerView 中存在一样的 Adapter,可以通过设置 RecyclerView.setRecycledViewPool(pool); 来共用一个 RecycledViewPool
public OuterRecyclerViewAdapter(List<Item> items) {
//Constructor stuff
viewPool = new RecyclerView.RecycledViewPool();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//Create viewHolder etc
holder.innerRecyclerView.setRecycledViewPool(viewPool);
}
现在所有的 inner RecyclerView都是同一个 view pool了, 这样就大大的减少了view的创建,提高性能
- RecycledViewPool是依据 ItemViewType 来索引ViewHolder的,所以必须确保共享的RecyclerView的Adapter是同一个,或view type 是不会冲突的。
- RecycledViewPool可以自主控制需要缓存的ViewHolder数量:
mPool.setMaxRecycledViews(itemViewType, number); - RecyclerView可以设置自己所需要的ViewHolder数量(默认是2),只有超过这个数量的 detached ViewHolder 才会丢进ViewPool中与别的RecyclerView共享。
recyclerView.setItemViewCacheSize(10); - 在合适的时机,RecycledViewPool会自我清除掉所持有的ViewHolder对象引用,不用担心池子会“侧漏”。当然也可以在认为合适的时机手动调用 clear()
然而,对于feed这边其实没啥作用,外部recyclerView触发recyler事件时,内部recyclerview并不会触发,导致Pool回不被填充。 这种方案仅适用于,内部recyclerView会滚动的情形
三、主动触发内层RecyclerView的回收
上文我们已经提及,RecyclerView的回收机制其实起源于滚动监听,对于嵌套RecyclerView而言,其外部的RecyclerView是能正常回收的,但是内部RecyclerView在无滚动的场景下其实是无法触发回收事件的
那么我们能不能想办法在 外部VH被回收时,主动触发下内部RecyclerView的回收呢? 因此,我们需要先感知到 外部VH被回收的时机!
主要翻阅源码的 onXXX事件,我们可以找到以下:
onViewRecycled()
onViewAttachedFromWindow()
onViewDetachedFromWindow()
onAttachedToRecyclerView()
onDetachedFromRecyclerView()
- onViewRecycled()
- 当 ViewHolder 已经确认被回收,且要放进 RecyclerViewPool 中前,该方法会被回调。
- RecyclerView 的回收机制在工作时,会先将移出屏幕的 ViewHolder 放进一级缓存中,当一级缓存空间已满时,才会考虑将一级缓存中已有的 ViewHolder 移到 RecyclerViewPool 中去。所以,并不是所有刚被移出屏幕的 ViewHoder 都会回调该方法。
- onViewAttachedFromWindow(ViewHolder) || onViewDetachedFromWindow(ViewHolder):
RecyclerView 本质上也是一个 ViewGroup,那么它的 Item 要显示出来,自然要 addView() 进来,移出屏幕时,自然要 removeView() 出去,对应的就是这两个方法的回调
需要注意的是:
如果调用了 notifyDataSetChanged() 的话,会触发所有 其CellItem 的 detached 回调先触发再触发 onAttached 回调 - onAttachedToRecyclerView() || onDetachedFromRecyclerView():
- 当 RecyclerView 调用了 setAdapter() 时会触发,旧的 adapter 回调 onDetached,新的 adapter 回调 onAttached
由此我们也知道了监听时机:
- 可以在外层RecyclerView的adapter的onViewRecycled回调中进行
- 或者是 内层RecyclerView的adapter的onViewDetachedFromWindow回调中进行
四、触发内层RecyclerView的回收
如何主动触发内层recyclerView的回收呢? 从源码看,我们找到了;
void removeAndRecycleViews() {
if (this.mItemAnimator != null) {
this.mItemAnimator.endAnimations();
}
if (this.mLayout != null) {
this.mLayout.removeAndRecycleAllViews(this.mRecycler);
this.mLayout.removeAndRecycleScrapInt(this.mRecycler);
}
this.mRecycler.clear();
}
但是这个方法是私有的,我们可以** A.通过反射实现调用**:
try {
ReflectUtils.invokeMethod(innerRecyclerView, "removeAndRecycleViews");
} catch (Exception e) {
e.printStackTrace();
}
public Object invokeMethod(Object owner, String methodName) throws Exception {
Class ownerClass = owner.getClass();
Method method = ownerClass.getDeclaredMethod(methodName);
method.setAccessible(true);
return method.invoke(owner);
}
继续追查源码,我们可以发现:
private void setAdapterInternal(@Nullable RecyclerView.Adapter adapter, boolean compatibleWithPrevious, boolean removeAndRecycleViews) {
...
if (!compatibleWithPrevious || removeAndRecycleViews) {
this.removeAndRecycleViews();
}
...
}
public void setAdapter(@Nullable RecyclerView.Adapter adapter) {
this.setLayoutFrozen(false);
this.setAdapterInternal(adapter, false, true);
this.processDataSetCompletelyChanged(false);
this.requestLayout();
}
在setAdapter内部,如果之前已经产生了view,会自动触发一次回收逻辑,那么我们可以通过** B. setAdapter(null) 去实现内层recylerView回收机制的触发**
五、setRecycleChildrenOnDetach
是不是觉得上边就完了?在探索的过程中我们有发现了LinearLayoutManager或其子类(如GridLayoutManager)有一个神奇的函数layout.setRecycleChildrenOnDetach(true)
它对应的效果是:
public void onDetachedFromWindow(RecyclerView view, Recycler recycler) {
super.onDetachedFromWindow(view, recycler);
if (this.mRecycleChildrenOnDetach) {
this.removeAndRecycleAllViews(recycler);
recycler.clear();
}
}
是不是很熟悉? 它不就对应着我们上述结论中:
- 主动触发内层RecyclerView的回收
- 内层RecyclerView的adapter的onViewDetachedFromWindow回调中进行
- 触发内层RecyclerView的回收
- 通过反射实现调用removeAndRecycleViews
因此,如果LayoutManager是LinearLayoutManager或其子类(如GridLayoutManager) 需要手动开启这个特性:layout.setRecycleChildrenOnDetach(true):
class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();
...
@Override
public void onCreateViewHolder(ViewGroup parent, int viewType) {
RecyclerView view = new RecyclerView(inflater.getContext());
LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(), LinearLayoutManager.HORIZONTAL);
innerLLM.setRecycleChildrenOnDetach(true);
innerRv.setLayoutManager(innerLLM);
innerRv.setRecycledViewPool(mSharedPool);
return new OuterAdapter.ViewHolder(innerRv);
}
}
六、延伸
在首次启动的实际场景中,会连续触发两次的OutAdapter的notify, 有时候会发现 第二个item的内层RecyclerView的内容空了,具体代码分析过程不再赘述,看下结论:
- OutAdapter第一次notify
- 触发0位置的 oncreate和onbind
- 触发内层RecyclerView的motify,并构建内层Cells
- 触发1位置的 oncreate和onbind
- 触发内层RecyclerView的motify,并构建内层Cells
- OutAdapter第二次notify
- 触发0位置的 oncreate和onbind
- 触发内层RecyclerView的motify,并构建内层Cells
- 由于1位置的不在屏幕范围内,调用移除操作
- 移动到mCachedViews; 并触发外部Adapter的onViewDetachedFromWindow(ViewHolder)
- remove 整个View操作会回调View的onDetachFromWindows
- 触发内层RecyclerView的onDetachFromWindows
- 触发内层Linearmanger的onDetachFromWindows, 由于内部设置了 setRecycleChildrenOnDetach(true)
- 触发内层RecyclerView的removeAndRecycleAllView去移除并缓存全部的Cells到Pool中
- 触发内部RecyclerView#adapter的Holder的onViewDetachedFromWindow
- 滑动
- 获取pos:1的Viewholder
- 从mCachedViews中命中,不调用onBinder,直接addView;
- 引发第二个item的recyclerView空白
这种情况,主要是要想办法让 Viewholder不要进入cacheViews,实现思路很多:
- 使用我们自己的方案,不借用setRecycleChildrenOnDetach
- 设置mCachedViews的大小;
- 外部Adapter的onViewDetachedFromWindow中,触发外部的cacheViews清空,或者不进入cacheViews, 或者设置相关标示
- 想方法外部的cell在attach时重新触发onbind,在外部Adapter#onViewAttachedToWindow中 再次触发下内部adapter的notify