RecyclerView的缓存机制,滑动10个,再滑回去,会有几个执行onBindView。
缓存的是什么?cachedView会执行onBindView吗?
1)首先说下RecycleView的缓存结构:
Recycleview有四级缓存,分别是mAttachedScrap(屏幕内),mCacheViews(屏幕外),mViewCacheExtension(自定义缓存),mRecyclerPool(缓存池)
mAttachedScrap(屏幕内),用于屏幕内itemview快速重用,不需要重新createView和bindView
mCacheViews(屏幕外),保存最近移出屏幕的ViewHolder,包含数据和position信息,复用时必须是相同位置的ViewHolder才能复用,应用场景在那些需要来回滑动的列表中,当往回滑动时,能直接复用ViewHolder数据,不需要重新bindView。
mViewCacheExtension(自定义缓存),不直接使用,需要用户自定义实现,默认不实现。
mRecyclerPool(缓存池),当cacheView满了后或者adapter被更换,将cacheView中移出的ViewHolder放到Pool中,放之前会把ViewHolder数据清除掉,所以复用时需要重新bindView。
2)四级缓存按照顺序需要依次读取。所以完整缓存流程是:
保存缓存流程:
插入或是删除itemView时,先把屏幕内的ViewHolder保存至AttachedScrap中
滑动屏幕的时候,先消失的itemview会保存到CacheView,CacheView大小默认是2,超过数量的话按照先入先出原则,移出头部的itemview保存到RecyclerPool缓存池(如果有自定义缓存就会保存到自定义缓存里),RecyclerPool缓存池会按照itemview的itemtype进行保存,每个itemType缓存个数为5个,超过就会被回收。
获取缓存流程:
AttachedScrap中获取,通过pos匹配holder——>获取失败,从CacheView中获取,也是通过pos获取holder缓存 ——>获取失败,从自定义缓存中获取缓存——>获取失败,从mRecyclerPool中获取 ——>获取失败,重新创建viewholder——createViewHolder并bindview。
3)了解了缓存结构和缓存流程,我们再来看看具体的问题 滑动10个,再滑回去,会有几个执行onBindView?
由之前的缓存结构可知,需要重新执行onBindView的只有一种缓存区,就是缓存池mRecyclerPool。
所以我们假设从加载RecyclView开始盘的话(页面假设可以容纳7条数据):
首先,7条数据会依次调用onCreateViewHolder和onBindViewHolder。
往下滑一条(position=7),那么会把position=0的数据放到mCacheViews中。此时mCacheViews缓存区数量为1,mRecyclerPool数量为0。然后新出现的position=7的数据通过postion在mCacheViews中找不到对应的ViewHolder,通过itemtype也在mRecyclerPool中找不到对应的数据,所以会调用onCreateViewHolder和onBindViewHolder方法。
再往下滑一条数据(position=8),如上。
再往下滑一条数据(position=9),position=2的数据会放到mCacheViews中,但是由于mCacheViews缓存区默认容量为2,所以position=0的数据会被清空数据然后放到mRecyclerPool缓存池中。而新出现的position=9数据由于在mRecyclerPool中还是找不到相应type的ViewHolder,所以还是会走onCreateViewHolder和onBindViewHolder方法。所以此时mCacheViews缓存区数量为2,mRecyclerPool数量为1。
再往下滑一条数据(position=10),这时候由于可以在mRecyclerPool中找到相同viewtype的ViewHolder了。所以就直接复用了,并调用onBindViewHolder方法绑定数据。
后面依次类推,刚消失的两条数据会被放到mCacheViews中,再出现的时候是不会调用onBindViewHolder方法,而复用的第三条数据是从mRecyclerPool中取得,就会调用onBindViewHolder方法了。
4)所以这个问题就得出结论了(假设mCacheViews容量为默认值2):
如果一开始滑动的是新数据,那么滑动10个,就会走10个bindview方法。然后滑回去,会走10-2个bindview方法。一共18次调用。
如果一开始滑动的是老数据,那么滑动10-2个,就会走8个bindview方法。然后滑回去,会走10-2个bindview方法。一共16次调用。
但是但是,实际情况又有点不一样。因为Recycleview在v25版本引入了一个新的机制,预取机制。
预取机制,就是在滑动过程中,会把将要展示的一个元素提前缓存到mCachedViews中,所以滑动10个元素的时候,第11个元素也会被创建,也就多走了一次bindview方法。但是滑回去的时候不影响,因为就算提前取了一个缓存数据,只是把bindview方法提前了,并不影响总的绑定item数量。
所以滑动的是新数据的情况下就会多一次调用bindview方法。
5)总结,问题怎么答呢?
四级缓存和流程说一下。
滑动10个,再滑回去,bindview可以是19次调用,可以是16次调用。
缓存的其实就是缓存item的view,在Recycleview中就是viewholder。
cachedView就是mCacheViews缓存区中的view,是不需要重新绑定数据的。
接下来,将尝试通过源码深入了解一下RecyclerView
中的缓存机制。
RecyclerView
是通过内部类Recycler
管理的缓存,那么Recycler
中缓存的是什么?我们知道RecyclerView
在存在大量数据时依然可以滑动的如丝滑般顺畅,而RecyclerView
本身是一个ViewGroup
,那么滑动时避免不了添加或移除子View(子View通过RecyclerView#Adapter中的onCreateViewHolder创建),如果每次使用子View都要去重新创建,肯定会影响滑动的流 畅性,所以RecyclerView
通过Recycler
来缓存的是ViewHolder
(内部包含子View),这样在滑动时可以复用子View,某些条件下还可以复用子View绑定的数据。所以本质上缓存是为了减少重复绘制View和绑定数据的时间,从而提高了滑动时的性能。
四级缓存
Recycler
缓存ViewHolder
对象有4个等级,优先级从高到底依次为:
- ArrayList<ViewHolder> mAttachedScrap
- ArrayList<ViewHolder> mCachedViews
- ViewCacheExtension mViewCacheExtension
- RecycledViewPool mRecyclerPool
注:官网上貌似把mAttachedScrap、mCachedViews当成一级了,为了方便区分,本文还是把他们当成两级缓存。
缓存 | 涉及对象 | 作用 | 重新创建视图View(onCreateViewHolder) | 重新绑定数据(onBindViewHolder) |
---|---|---|---|---|
一级缓存 | mAttachedScrap | 缓存屏幕中可见范围的ViewHolder | false | false |
二级缓存 | mCachedViews | 缓存滑动时即将与RecyclerView分离的ViewHolder,按子View的position或id缓存,默认最多存放2个 | false | false |
三级缓存 | mViewCacheExtension | 开发者自行实现的缓存 | - | - |
四级缓存 | mRecyclerPool | ViewHolder缓存池,本质上是一个SparseArray,其中key是ViewType(int类型),value存放的是 ArrayList< ViewHolder>,默认每个ArrayList中最多存放5个ViewHolder | false | true |
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;
}
上述逻辑用流程图表示:
image.png
总结一下上述流程:通过mAttachedScrap、mCachedViews及mViewCacheExtension获取的ViewHolder不需要重新创建布局及绑定数据;通过缓存池mRecyclerPool获取的ViewHolder不需要重新创建布局,但是需要重新绑定数据;如果上述缓存中都没有获取到目标ViewHolder,那么就会回调Adapter#onCreateViewHolder创建布局,以及回调Adapter#onBindViewHolder来绑定数据。
ViewCacheExtension
我们已经知道ViewCacheExtension属于第三级缓存,需要开发者自行实现,那么ViewCacheExtension在什么场景下使用?又是如何实现的呢?
首先我们要明确一点,那就是Recycler
本身已经设置了好几级缓存了,为什么还要留个接口让开发者去自行实现缓存呢?关于这一点,谈一谈我的理解:来看看Recycler
中的其他缓存,其中mAttachedScrap
用来处理可见屏幕的缓存;mCachedViews
里存储的数据虽然是根据position
来缓存,但是里面的数据随时可能会被替换的;再来看mRecyclerPool
,mRecyclerPool
里按viewType
去存储ArrayList< ViewHolder>
,所以mRecyclerPool
并不能按position
去存储ViewHolder
,而且从mRecyclerPool
取出的View
每次都要去走Adapter#onBindViewHolder
去重新绑定数据。假如我现在需要在一个特定的位置(比如position=0位置)一直展示某个View,且里面的内容是不变的,那么最好的情况就是在特定位置时,既不需要每次重新创建View,也不需要每次都去重新绑定数据,上面的几种缓存显然都是不适用的,这种情况该怎么办呢?可以通过自定义缓存ViewCacheExtension
实现上述需求。
- ViewCacheExtension适用场景:ViewHolder位置固定、内容固定、数量有限时使用
- ViewCacheExtension使用举例:
比如在position=0时展示的是一个广告,位置不变,内容不变,来看看如何实现:
DemoRvActivity.java:
public class DemoRvActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private DemoAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo_rv);
recyclerView = findViewById(R.id.rv_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
adapter = new DemoAdapter();
recyclerView.setAdapter(adapter);
//viewType类型为TYPE_SPECIAL时,设置四级缓存池RecyclerPool不存储对应类型的数据 因为需要开发者自行缓存
recyclerView.getRecycledViewPool().setMaxRecycledViews(DemoAdapter.TYPE_SPECIAL, 0);
//设置ViewCacheExtension缓存
recyclerView.setViewCacheExtension(new MyViewCacheExtension());
}
//实现自定义缓存ViewCacheExtension
class MyViewCacheExtension extends RecyclerView.ViewCacheExtension {
@Nullable
@Override
public View getViewForPositionAndType(@NonNull RecyclerView.Recycler recycler, int position, int viewType) {
//如果viewType为TYPE_SPECIAL,使用自己缓存的View去构建ViewHolder
// 否则返回null,会使用系统RecyclerPool缓存或者从新通过onCreateViewHolder构建View及ViewHolder
return viewType == DemoAdapter.TYPE_SPECIAL ? adapter.caches.get(position) : null;
}
}
}
在看下Adapter的代码:
public class DemoAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
//viewType类型 TYPE_COMMON代表普通类型 TYPE_SPECIAL代表特殊类型(此处的View和数据一直不变)
public static final int TYPE_COMMON = 1;
public static final int TYPE_SPECIAL = 101;
public SparseArray<View> caches = new SparseArray<>();//开发者自行维护的缓存
private List<String> mDatas = new ArrayList<>();
DemoAdapter() {
initData();
}
private void initData() {
for (int i = 0; i < 50; i++) {
if (i == 0) {
mDatas.add("我是一条特殊的数据,我的位置固定、内容不会变");
} else {
mDatas.add("这是第" + (i + 1) + "条数据");
}
}
}
public List<String> getData() {
return mDatas;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
Log.e("TTT", "-----onCreateViewHolder:" + "viewType is " + viewType + "-----");
Context context = viewGroup.getContext();
if (viewType == TYPE_SPECIAL) {
View view = LayoutInflater.from(context)
.inflate(R.layout.item_special_layout, viewGroup, false);
return new SpecialHolder(view);
} else {
View view = LayoutInflater.from(context)
.inflate(R.layout.item_common_layout, viewGroup, false);
return new CommonHolder(view);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
Log.e("TTT", "-----onBindViewHolder:" + "position is " + position + "-----");
if (holder instanceof SpecialHolder) {
SpecialHolder sHolder = (SpecialHolder) holder;
sHolder.tv_ad.setText(mDatas.get(position));
//这里是重点,根据position将View放到自定义缓存中
caches.put(position, sHolder.itemView);
} else if (holder instanceof CommonHolder) {
CommonHolder cHolder = (CommonHolder) holder;
cHolder.tv_textName.setText(mDatas.get(position));
}
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return TYPE_SPECIAL;//第一个位置View和数据固定
} else {
return TYPE_COMMON;
}
}
@Override
public long getItemId(int position) {
return super.getItemId(position);
}
@Override
public int getItemCount() {
return mDatas.size();
}
class SpecialHolder extends RecyclerView.ViewHolder {
TextView tv_ad;
public SpecialHolder(@NonNull View itemView) {
super(itemView);
tv_ad = itemView.findViewById(R.id.tv_special_ad);
}
}
class CommonHolder extends RecyclerView.ViewHolder {
TextView tv_textName;
public CommonHolder(@NonNull View itemView) {
super(itemView);
tv_textName = itemView.findViewById(R.id.tv_text);
}
}
}
运行界面如下:
重点关注第一条数据,当第一次运行时,在针对于第一条数据会执行Adapter#onCreateViewHolder
和Adapter#onBindViewHolder
,想想也对,毕竟第一次执行,肯定要有一个创建View
和绑定数据的过程。此时向下滑动到底部再滑上来,通过debug发现不再走这两个方法了,而是在getViewForPositionAndType
回调中根据position
拿到了我们自定义缓存中的View
及数据,所以可以直接展示。再看我们自己维护的缓存是什么时候设置的,其实我这里是在Adapter#onBindViewHolder
中根据position
设置的缓存:
caches.put(position, sHolder.itemView);
假如我们把上面这行代码删除了呢,再次执行上述滑动操作,自定义缓存对应失效了,Adapter#onCreateViewHolder
和Adapter#onBindViewHolder
都会被执行,这里可能大家可能会有个疑问,自定义缓存失效,为什么RecyclerPool
里也没有对这个viewType
进行缓存呢(因为如果缓存了,是不会重新执行onCreateViewHolder的)?猜想这是因为我在代码中设置了
recyclerView.getRecycledViewPool().setMaxRecycledViews(DemoAdapter.TYPE_SPECIAL, 0)
即viewType
类型为TYPE_SPECIAL
时,设置缓存池RecyclerPool
不存储对应类型的数据,因为开发者自行缓存了,所以没必要再往RecyclerPool
存储了,如果把上面这行代码注释掉,重新执行上述滑动操作,会发现针对第一条数据只执行了Adapter#onBindViewHolder
,因为即使自定义缓存失效了,默认还是会往RecyclerPool
存储的嘛,这也验证了我们的猜想。
RecyclerView & ListView缓存机制对比
结论援引自:Android ListView 与 RecyclerView 对比浅析--缓存机制
ListView和RecyclerView缓存机制基本一致:
1). mActiveViews和mAttachedScrap功能相似,意义在于快速重用屏幕上可见的列表项ItemView,而不需要重新createView和bindView;
2). mScrapView和mCachedViews + mReyclerViewPool功能相似,意义在于缓存离开屏幕的ItemView,目的是让即将进入屏幕的ItemView重用.
3). RecyclerView的优势在于a.mCacheViews的使用,可以做到屏幕外的列表项ItemView进入屏幕内时也无须bindView快速重用;b.mRecyclerPool可以供多个RecyclerView共同使用,在特定场景下,如viewpaper+多个列表页下有优势.客观来说,RecyclerView在特定场景下对ListView的缓存机制做了补强和完善。
不同使用场景:列表页展示界面,需要支持动画,或者频繁更新,局部刷新,建议使用RecyclerView,更加强大完善,易扩展;其它情况(如微信卡包列表页)两者都OK,但ListView在使用上会更加方便,快捷。
作者:_小马快跑_
链接:https://www.jianshu.com/p/e1b257484961
来源:简书
参考: