RecyclerView
内存优越性,得益于它独特的缓存机制。
1 如何复用表项
如果列表中的每个表项在移出屏幕时被销毁,移入时又被重新创建,是很消耗资源,所以RecyclerView
引入了缓存机制。缓存是为了复用,复用的好处是有可能免去两个耗费资源的操作:创建表项视图和为每个表项视图绑定数据。
2 ViewHolder
2.1 作用
ViewHolder
是对RecyclerView
上的itemView
的封装,它是RecyclerView
缓存的载体。它封装了以下属性:
View itemView
:对应RecyclerView
的子View
;int mPosition
:View
当前对应数据在数据源中的位置;int mOldPosition
:View
上次绑定的数据在数据源中的位置;long mItemId
:可以判断ViewHolder
是否需要重新绑定数据;int mItemViewType
:itemView
对应的类型;int mPreLayoutPosition
:在预布局阶段ViewHolder
对应数据在数据源中的位置;int mFlags
:ViewHolder
对应的标记位;List<Ojbect> mPayloads
:实现局部刷新;Recycler mScrapContainer
:如果不为空,表示ViewHolder
是存放在scrap
缓存中;
2.2 flag
FLAG_BOUND
:ViewHolder
对应的View
已经绑定好了数据,无需重新绑定FLAG_UPDATE
:数据发生了变化,View
需要重新绑定FLAG_INVALID
:数据失效了,View
需要重新绑定FLAG_REMOVED
:数据从数据源中删除,View
在消失动画中仍然有用FLAG_NOT_RECYCLABLE
:ViewHolder
不能被回收,ViewHolder
对应ItemView
做动画时需要保证ViewHolder
不能被回收掉FLAG_RETURNED_FROM_SCRAP
:从scrap
缓存中获取到的ViewHolder
FLAG_IGNORE
:如果回收该类型的ViewHolder
会报错FLAG_TMP_DETACHED
:表示ItemView
从RecyclerView上DETACHED
了,detach
和remove
的区别是,remove
会将View
从ViewGroup
的children
数组中删除并且刷新ViewGroup
,detach
只会删除不会触发刷新FLAG_ADAPTER_FULLUPDATE
:表示ViewHolder
需要全量更新,如果没有设置该标志位,则是局部更新FLAG_MOVED
:当ViewHolder
的位置发生变化,做动画时需要使用FLAG_APPEARED_IN_PRE_LAYOUT
:ViewHolder
出现在预布局中,需要做APPEARED
动画
3 RecyclerView
局部刷新
RecyclerView
的数据更新,主要有以下几个方法:
notifyDataSetChanged()
:刷新全部可见的ViewHolder
;notifyItemChanged(int position)
:刷新指定位置的ViewHolder
;notifyItemChanged(int position, @Nullable Object payload)
:刷新指定位置的ViewHolder
,其中playload
参数可以认为是要刷新的一个标示;notifyItemRangeChanged(int positionStart, int itemCount)
:从指定的位置开始刷新指定个数的ViewHolder
;notifyItemRangeChanged(int positionStart, int itemCount, Object payload)
:从指定的位置开始刷新指定个数的ViewHolder
,其中playload
参数可以认为是要刷新的一个标示;notifyItemInserted(int position)
、notifyItemMoved(int fromPosition, int toPosition)
、notifyItemRangeInserted(int positionStart, int itemCount)
、notifyItemRemoved(int position)
、notifyItemRangeRemoved(int positionStart, int itemCount)
:插入、移动、移除并自动刷新;
@Override
public void onBindViewHolder(ViewHolderholder, int position, List<Object> payloads) {
if (payloads.isEmpty()) {
// payloads为空,说明是更新整个ViewHolder
onBindViewHolder(holder, position);
} else {
// payloads不为空,这只更新需要更新的View即可。
String payload = payloads.get(0).toString();
if ("changeColor".equals(payload)) {
holder.textView.setTextColor("");
}
}
}
4 四级缓存
以下是RecylcerView
缓存机制的时序图:
其中,Recycler
用于表项的复用,RecyclerView
通过Recycler
获得下一个待绘制的表项。RecyclerView
缓存基本上是通过三个内部类管理的,Recycler
、RecycledViewPool
和ViewCacheExtension
,以下是RecyclerView.Recycler
的部分源码:
public class RecyclerView extends ViewGroup implements ScrollingView {
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;
}
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
}
public abstract static class ViewCacheExtension {
@Nullable
public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position, int type);
}
}
scrap [skræp] 碎片,小块; attach [əˈtætʃ] 系上,贴上 detached [dɪˈtætʃt] 单独的,分离的 extension [ɪkˈstenʃn] 延伸,扩展
由上可知,Recycler
有4
个变量用来缓存ViewHolder
对象,优先级由高到低依次为ArrayList<ViewHolder> mAttachedScrap
、ArrayList<ViewHolder> mCachedViews
、ViewCacheExtension mViewCacheExtension
、RecycledViewPool mRecyclerPool
。RecycledViewPool
对ViewHolder
按viewType
分类存储(通过SparseArray
),同类ViewHolder
存储在默认大小为5
的ArrayList
中。
以下是它们的含义:
RecyclerView
的四级缓存:
mChangedScrap
与mAttachedScrap
是用来做屏幕内ViewHolder
复用,不需要重新onCreateViewHolder
和onBindViewHolder
;mCachedView
是用来缓存最近移出屏幕的ViewHolder
,包括数据和position
信息。复用时必须是是相同位置的ViewHolder
,应用场景是在那些来回滑动的列表中,往回滑动时,能直接复用ViewHolder
的数据,不用onBindeViewHolder
;mViewCacheExtension
,自定义缓存,这个的创建和缓存完全由开发者自己控制,系统未在这里添加数据;RecycledViewPool
,ViewHolder
缓存池,当mCacheView
满后或者adapter
被更换,将mCacheView
中移出的ViewHolder
放到缓存池中,同时把ViewHolder
的数据清除掉,所以复用的时候需要onBindeViewHolder
;
4.1 屏幕内缓存/一级缓存/scrap
缓存(mAttachedScrap
和mChangedScrap
)
屏幕内缓存指在屏幕中显示的ViewHolder
,这些ViewHolder
会缓存在mAttachedScrap
、mChangedScrap
中:
mAttachedScrap
中存放的是没有与RecyclerView
分离的ViewHolder
列表,从其中复用的ViewHolder
既不需要重新创建也不需要重新绑定数据;mChangedScrap
表示数据已经改变的ViewHolder
列表,只有满足以下的条件ViewHolder
才会被添加到mChangedScrap
中:当它关联的itemView
发生了变化(notifyItemChanged
或者notifyItemRangeChanged
被调用),并且ItemAnimator
调用ViewHolder#canReuseUpdatedViewHolder
方法时,返回了false
(返回false
表示要执行用一个view
替换另一个view
的动画,例如淡入淡出动画。true
表示动画在view
内部发生)。否则,ViewHolder
会被添加到mAttachedScrap
中;
mChangedScrap
和mAttachedScrap
可以看做是一个层级,都是屏幕上可见ViewHolder
,只不过区分了状态(改变和未改变)。
4.2 屏幕外缓存/二级缓存(mCachedViews
)
当列表滑动出了屏幕时,ViewHolder
会被缓存在mCachedViews
,其大小由mViewCacheMax
决定,默认DEFAULT_CACHE_SIZE
为2
,可通过Recyclerview.setItemViewCacheSize()
动态设置。
从mCachedViews
中复用的ViewHolder
,只能复用于指定位置的表项。也可以说,mCachedViews
是离屏缓存,用于缓存指定位置的ViewHolder
,只有“列表回滚”这一种场景(刚滚出屏幕的表项再次进入屏幕),才有可能命中该缓存。该缓存存放在默认大小为2
的ArrayList
中;
4.3 自定义缓存/三级缓存(ViewCacheExtension
)
可以自己实现ViewCacheExtension
类实现自定义缓存,可通过Recyclerview.setViewCacheExtension()
设置。
4.4 缓存池/四级缓存(RecycledViewPool
)
ViewHolder
在首先会缓存在mCachedViews
中,当超过了个数(比如默认为2
),就会添加到RecycledViewPool
中。从mRecyclerPool
中复用的ViewHolder,
只能复用于viewType
相同的表项,并且需要重新绑定数据。
RecycledViewPool
会根据每个ViewType
把ViewHolder
分别存储在不同的列表中,每个ViewType
最多缓存DEFAULT_MAX_SCRAP = 5 x ViewHolder
,如果RecycledViewPool
没有被多个RecycledView
共享,对于线性布局,每个ViewType
最多只有一个缓存,如果是网格有多少行就缓存多少个。它们之间的关系如下 :
5 缓存策略
缓存流程:
- 在插入或删除
ViewHolder
的时候,首先会把屏幕内的ViewHolder
保存至mAttachedScrap
中; - 滑动屏幕时,首先消失的
ViewHolder
会保存到mCachedViews
中,mCachedView
的大小默认是2
,超过的话,就按照先入先出的原则,将移出头部的ViewHolder
保存到RecyclerPool
缓存池(如果有自定义的缓存就保存到自定义缓存中)。RecyclerPool
缓存池会按照ViewHolder
的viewType
进行保存,每个viewType
缓存个数为5
个,超过了就会被回收;
获取缓存流程:首先从mAttachedScrap
中获取,通过position
匹配ViewHolder
,如果匹配失败,就会从mCachedViews
中获取,也是通过position
获取ViewHolder
缓存;如果获取失败,就会继续从自定义缓存中获取;如果获取失败,会继续从mRecyclerPool
中获取;如果获取失败,会重新创建ViewHolder
。 流程如下 :
Recyclerview
在获取ViewHolder
时按四级缓存的顺序查找,如果没找到就创建(createViewHolder
)。其中只有RecycledViewPool
找到时才会调用 onBindViewHolder
,其它缓存不会重新onBindViewHolder
。
问:在RecyclerView
中,滑动10
个ViewHolder
,再滑动回去,会有几个ViewHolder
执行onBindViewHolder
?
假设页面可以容纳7
条数据
- 首先,页面内的
7
条数据会依次调用onCreateViewHolder
和onBindViewHolder
; - 之后,向下滑动
1
条(position = 7
),会把position = 0
的数据存放到mCachedViews
中,此时mCachedViews
缓存数量为1
,RecyclerPool
中缓存数量为0
。position = 7
的数据通过position
在mCachedViews
找不到对应的ViewHolder
,通过viewType
在mRecyclerPool
中找不到对应的ViewHolder
,所以会调用onCreateViewHolder
和onBindViewHolder
方法; - 再往下滑动
1
条(position = 8
),同position = 7
,position = 1
的数据会存放到mCachedViews
; - 再往下滑动
1
条(position = 9
),由于mCahcedViews
的缓存区默认容量为2
,所以,position = 0
的数据会被清空,存放到mRecyclerPool
缓存池中,而position = 2
的数据会存放到mCachedViews
,而position = 9
的数据无法通过position
在mCacheViews
中获取,也无法通过viewType
在mRecyclerPool
中获取,所以还会调用onCreateViewHolder
和onBindViewHolder
。此时,mCachedViews
缓存区数量是2
,mRecyclerPool
的数量为1
; - 再往下滑动
1
条(position = 10
),这个时候就可以通过viewType
在mRecyclerPool
中查找到ViewHolder
,所以可以直接复用了,并通过onBindeViewHolder
来绑定数据; - 依次类推
RecyclerView
中,并不是每次绘制表项都会重新创建ViewHolder
对象,也不是每次都会重新绑定ViewHolder
数据。
- 最坏的情况:重新创建
ViewHolder
并绑定数据; - 次好的情况:复用
ViewHolder
但重新绑定数据; - 最好的情况:复用
ViewHolder
且不重新绑定数据;
通过了解RecyclerView
的四级缓存,可以知道,RecyclerView
最多可以缓存N(屏幕最多可显示的item数) + 2(屏幕外的缓存) + 5 x M (M代表M个ViewType,缓存池的缓存)
,只有RecycledViewPool
找到时才会重新调用bindViewHolder
。
还需要注意的是,RecycledViewPool
可以被多个RecyclerView
共享,其缓存个数与ViewType
个数、布局相关,如果RecycledViewPool
没有被多个RecycledView
共享,对于线性布局,每个ViewType
最多只有一个缓存,如果是网格布局有多少行就缓存多少个。
参考
https://juejin.cn/post/6844903778303344647
https://zhooker.github.io/2017/08/14/关于Recyclerview的缓存机制的理解/
http://www.lxiaoyu.com/p/286763
https://www.jianshu.com/p/b11c62871169
https://jishuin.proginn.com/p/763bfbd55a86
https://www.jianshu.com/p/443d741c7e3e