前言
RecyclerView 是一个好用又复杂的控件,其功能的高度解耦化,规范化的 ViewHolder 写法,以及对动画的友好支持,都是它与传统 ListView 的区别。
它有几大模块:
- LayoutManager:控制 item 的布局
- RecyclerView.Adapter:为 RecyclerView 提供数据
- ItemDecoration:为 RecyclerView 添加分割线
- ItemAnimator:控制 item 的动画
- Recycler:负责回收和提供 View,和 RecyclerView 的复用机制相关
下面就从源码(API 28)角度分析 RecyclerView,RecyclerView 的源码很复杂,很难在一篇文章内讲完,所以打算分几篇来讲,本文是第一篇,将围绕 RecyclerView 的内部类 Recycler 展开分析:
RecyclerView.Recycler
首先看一下它的作用,源码上是这样写的:
A Recycler is responsible for managing scrapped or detached item views for reuse.
意思就是 Recycler 负责管理废弃或被 detached 的 item 视图,以便重复利用。
它有以下几个成员变量:
主要成员变量
1 |
|
这几个成员变量都和 RecyclerView 的缓存相关,如果按照四级缓存的话,它们可以这样划分:
第一级缓存:mAttachedScrap、mChangedScrap
第二级缓存:mCachedViews
第三级缓存:ViewCacheExtension
第四级缓存:RecycledViewPool
后面再介绍 mAttachedScrap、mChangedScrap、mCachedViews 具体存的是哪些 ViewHolder。
现在先了解下 RecycledViewPool 和 ViewCacheExtension这两个类:
RecyclerView.Recycler
首先看一下它的作用,源码上是这样写的:
A Recycler is responsible for managing scrapped or detached item views for reuse.
意思就是 Recycler 负责管理废弃或被 detached 的 item 视图,以便重复利用。
它有以下几个成员变量:
主要成员变量
1 |
|
这几个成员变量都和 RecyclerView 的缓存相关,如果按照四级缓存的话,它们可以这样划分:
第一级缓存:mAttachedScrap、mChangedScrap
第二级缓存:mCachedViews
第三级缓存:ViewCacheExtension
第四级缓存:RecycledViewPool
后面再介绍 mAttachedScrap、mChangedScrap、mCachedViews 具体存的是哪些 ViewHolder。
现在先了解下 RecycledViewPool 和 ViewCacheExtension这两个类:
RecycledViewPool
继续先看官方注释:
RecycledViewPool lets you share Views between multiple RecyclerViews.
RecycledViewPool 用于在多个 RecyclerView 间共享 View。
在使用时,只需创建 RecycledViewPool 实例,然后调用 RecyclerView 的 setRecycledViewPool(RecycledViewPool) 方法即可。
RecycledViewPool 存储在 Recycler 中,通过 Recycler 存取。
成员变量
RecycledViewPool 有一个重要的成员变量:1
2// SparseArray 类似于 key 为 int 类型 的 HashMap
SparseArray<ScrapData> mScrap = new SparseArray<>();
其中 ScrapData 的定义如下:1
2
3
4
5
6static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP; // 5
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
mScrap 是一个 <int, ScrapData> 的映射,其中 int 代表了 viewType,ScrapData 则存储了一个 ViewHolder 集合。
主要方法
getScrapDataForType
1 | private ScrapData getScrapDataForType(int viewType) { |
该方法根据 viewType 获取相应的 ScrapData,如果该 viewType 还没有绑定 ScrapData,就新创建一个 ScrapData 并绑定到该 viewType。
setMaxRecycledViews
1 | public void setMaxRecycledViews(int viewType, int max) { |
该方法可以设置相应 viewType 的 View 容量,超出容量时,从后面开始删除,直到满足新的容量。
getRecycledView
1 | public ViewHolder getRecycledView(int viewType) { |
该方法根据 viewType 获取一个 ViewHolder,获取到的 ViewHolder 将会被移除出 Scrap 堆。获取不到则返回 null。
putRecycledView
1 | public void putRecycledView(ViewHolder scrap) { |
该方法也很好理解,根据 ViewHolder 的 viewType 放入 RecycledViewPool 的相应集合中,如果集合已满,不再添加。
接下来看另一个类:
ViewCacheExtension
ViewCacheExtension 是一个由开发者控制的 View 缓存帮助类,其定义如下:1
2
3
4
5
6
7
8
9public abstract static class ViewCacheExtension {
/**
* Returns a View that can be binded to the given Adapter position.
*/
public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,
int type);
}
开发者可以实现这个抽象类,通过调用 RecyclerView 的 setViewCacheExtension(ViewCacheExtension) 方法设置,最终将 ViewCacheExtension 存储在 Recycler 中。
当调用 Recycler 的 getViewForPosition 方法时,如果 attached scrap 和 已经缓存都没有找到合适的 View,就会调用 ViewCacheExtension 的 getViewForPositionAndType 方法来获取 View。
需要注意的是,Recycler 不会对这个类做任何缓存处理,是否需要缓存 View 由开发者自己控制。
主要方法
看完这两个类,现在回到 Recycler 中,看一下 Rcycler 的主要方法:
getViewForPosition
getViewForPosition 方法比较重要,用于获取某个位置需要展示的 View,如下:1
2
3
4
5
6
7public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
继续看 tryGetViewHolderForPositionByDeadline 方法,该方法会依次从几个缓存中获取,分别来看一下:1
2
3
4
5
6 // 如果是处于预布局阶段(先简单理解为执行 dispatchLayoutStep1 方法)
// (其实下面方法要返回 ture 还需要开启“预处理动画”,这跟动画有关,先不多说)
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
第一步,从 mChangedScrap 中获取,获取不到就返回 null。
如果 holder 还是为 null,执行下面代码:1
2
3
4
5
6
7
8
9
10
11
12
13// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
// 回收无效的 ViewHolder
// ...
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
第二步,根据 position 依次从 mAttachedScrap、mHiddenViews(存储在 ChildHelper 类)、mCachedViews 中获取缓存的 ViewHolder。
可以从 mHiddenViews 获取到缓存的话,就将其从 mHiddenViews 移除并添加到 Scrap 缓存(根据情况添加到 mAttachedScrap 或 mChangedScrap)。可以从 mCacheViews 中获取到缓存的话,就将其从 mCacheViews 移除。
获取到后,发现无效的话,将对获取到的 ViewHolder 进行清理并回收(放入 mCachedViews 或 RecycledViewPool)。
获取不到,就继续往下执行:1
2
3
4
5
6
7
8
9
10 // 默认返回 false,可通过 Adapter.setHasStableIds 方法设置该值
if (mAdapter.hasStableIds()) {
// 根据 id 依次在 mAttachedScrap、mCachedViews 中获取缓存
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
第三步,根据 id 依次从 mAttachedScrap、mCachedViews 中获取缓存,还没有获取到就继续往下:
1 | // 如果用户设置了 ViewCacheExtension |
第四步,从用户设置的 ViewCacheExtension 中获取缓存,没有获取到就继续往下:1
2
3
4if (holder == null) { // fallback to pool
holder = getRecycledViewPool().getRecycledView(type);
// ...
}
第五步,根据 viewType 从 RecycledViewPool 中得到缓存。
RecycledViewPool 已经是最后一级缓存了,如果这里也没有获取到,只能通过 Adapter 的 createViewHolder 方法创建一个 ViewHolder:1
2
3
4
5
6if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
// ...
}
最后小结一下获取某个位置的 View 的过程:
- 先后根据 position 或 id 从 mChangedScrap 中获取缓存
- 根据 position 依次从 mAttachedScrap、mHiddenViews(存储在 ChildHelper 类)、mCachedViews 中获取缓存
- 根据 id 依次从 mAttachedScrap、mCachedViews 中获取缓存
- 从用户设置的 ViewCacheExtension 中获取缓存
- 从 RecycledViewPool 中得到缓存的废弃 ViewHolder
- 通过 Adapter 的 createViewHolder 方法创建一个 ViewHolder
另外,光有视图还不够,还要为视图设置数据,所以后面还有这样一段代码:1
2
3
4
5
6// ViewHolder 还未进行绑定操作、带有 FLAG_UPDATE 或 FLAG_INVALID 标记
else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
// ...
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
tryBindViewHolderByDeadline 方法最后会调用 Adapter 的 onBindViewHolder 方法,我们平时就是在该方法上给视图设置数据的。
recycleView
既然叫 Recycler,那肯定要做回收工作了,recycleView 方法就完成了这些工作,下面看一下该方法的实现:1
2
3
4
5public void recycleView(@NonNull View view) {
ViewHolder holder = getChildViewHolderInt(view);
// ...
recycleViewHolderInternal(holder);
}
继续看 recycleViewHolderInternal:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 void recycleViewHolderInternal(ViewHolder holder) {
// ...
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
int cachedViewSize = mCachedViews.size();
// 若 CacheViews 达到最大容量(2),将最老的缓存从 CacheViews 移除,并添加到 RecycledViewPool 中
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
// ...
// 将 View 缓存到 mCachedViews 中
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
// 没有添加到 mCachedViews 的话,就添加到 RecycledViewPool 中
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
// ...
}
可以看到,回收过程主要涉及到两层缓存,第一层缓存是 CacheViews,在添加时,如果发现原来的 CacheViews 已经达到最大容量,就将最老的缓存从 CacheViews 移除,并添加到 RecycledViewPool。第二层缓存是 RecycledViewPool,如果不能添加到 mCacheViews,就会添加到 RecycledViewPool 中。
补充
mChangedScrap 和 mAttachedScrap 中的 View 从何而来
从前面可以得知,在执行 Recycler 的 recycleView 方法时,会将回收的 View 缓存到 mCahceViews 或 recycledViewPool 中,那么另外两个 Scrap 缓存(mChangedScrap 和 mAttachedScrap)中的 View 是何时添加进来的呢?
无论是 mAttachedScrap 还是 mChangedScrap ,它们获得 View 的途径都只有一个,那就是通过 Recycler 的 scrapView 方法。先看下该方法:
Recycler#scrapView
1 | void scrapView(View view) { |
该方法通过判断 ViewHolder 的 flag 以及是否设置 ItemAnimator 等,决定将 View 缓存到 mAttachedScrap 还是 mChangedScrap。
那么该方法在何时调用呢?有两种情况:
- 以 LinearLayoutManager 为例,在它的 onLayoutChildren 方法中,会调用
1
detachAndScrapAttachedViews(recycler);
该方法定义在 RecyclerView 的 LayoutManager 中,它继续调用 scrapOrRecycleView 方法,如果在该方法符合条件就调用 Recycler 的 scrapView 方法。
- 通过 mHiddenViews 获取到缓存时,也会调用 scrapView 方法。
场景分析
下面就根据一些场景来分析下 Recycler 是如何进行回收和复用的。
第一次 layout
由于这里不是专门分析 layout 过程的,就不从 onLayout 开始说了,中间的过程省略掉,它最终会调用到 LayoutManager 的 onLayoutChildren,这里以 LinearLayoutManager 为例:
LinearLayoutManager#onLayoutChildren
1 |
|
首先看(1)处,detachAndScrapAttachedViews 方法会根据情况将子 View 回收到相应缓存,具体过程之后再看,由于现在是第一次 layout,RecyclerView 中没有子 View,所以现在该方法没啥用。
接下来看(2)处,这里的 fill 方法比较重要,它的作用是填充布局。看一下该方法:
LinearLayoutManager#fill
1 | int fill(RecyclerView.Recycler recycler, LayoutState layoutState, |
主要看(1)处的 layoutChunk 方法,只要还有需要填充的空间,就会不断调用该方法:
LinearLayoutManager#layoutChunk
1 | void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, |
(2)处的 addView 方法就不多说了,该方法将得到的子 View 添加到 RecyclerView 中。主要看(1)处,看看子 View 从何而来:1
2
3
4
5
6
7View next(RecyclerView.Recycler recycler) {
// ...
final View view = recycler.getViewForPosition(mCurrentPosition);
return view;
}
这个方法是不是很熟悉呢?没错,它就是之前分析的 Recycler 的 getViewForPosition 方法。
不过由于现在没有任何缓存,所以第一次 layout 的时候是通过 Adapter 的 createViewHolder 来创建子 View的,并且没有添加任何缓存。
更新列表
更新列表可以使用 Adapter 的一系列 notify 方法,这里分析其中两个方法:notifyDataSetChanged 和 notifyItemChanged(int)。
Adapter#notifyDataSetChanged
该方法最终调用了 RecyclerViewDataObserver 的 onChanged 方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void onChanged() {
// ...
// 该方法主要做了这两件事
// 1. 给所有 ViewHolder 添加了 FLAG_UPDATE 和 FLAG_INVALID
// 2. 默认情况下(mHasStableIds 为 false)清空 CacheViews
processDataSetCompletelyChanged(true);
if (!mAdapterHelper.hasPendingUpdates()) {
// 进行视图重绘
requestLayout();
}
}
该方法会进行视图重绘,又来到了 layout 过程,继续以 LinearLayoutManager 为例,从它的 onLayoutChildren 方法看起,由于分析第一次 layout 时已经看过一遍了,这次主要看下不同之处:
1 |
|
主要区别在于 detachAndScrapAttachedViews 方法,这次它开始起作用了,该方法在 RecyclerView 的 LayoutManager 中定义,看下它的实现:
LayoutManager#detachAndScrapAttachedViews
1 | public void detachAndScrapAttachedViews(@NonNull Recycler recycler) { |
由于不是第一次 layout,RecyclerView 这时已经有子 View 了,该方法遍历子 View,调用 scrapOrRecycleView 方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
// 不能回收添加了 FLAG_IGNORE 标记的 ViewHolder
// 可通过 LayoutManager 的 ignoreView 为相应的 View 添加该标记
if (viewHolder.shouldIgnore()) {
return;
}
// 这些条件都满足,进入 if 块
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
// ...
}
}
这里将子 View 移除并通过 Recycler 的 recycleViewHolderInternal 方法进行回收:
Recycler#recycleViewHolderInternal
1 | void recycleViewHolderInternal(ViewHolder holder) { |
最终被移除的子 View 缓存到了 RecycledViewPool 中。
后面在调用 fill 方法进行布局填充时,就可以从 RecycledViewPool 中拿取缓存的 View。
Adapter#notifyItemChanged
该方法传入一个 int 参数,表示要数据有更新的 item 的 position。1
2
3public final void notifyItemChanged(int position) {
mObservable.notifyItemRangeChanged(position, 1);
}
最终调用 RecyclerViewDataObserver 的 onItemRangeChanged 方法:1
2
3
4
5
6
7
8
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
// 会在 mAdapterHelper 中创建一个 UpdateOp,将信息保存起来
if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
// 如果可以进行更新操作,执行该方法
triggerUpdateProcessor();
}
}
继续看 triggerUpdateProcessor 方法:1
2
3
4
5
6
7
8
9void triggerUpdateProcessor() {
// 判断条件默认为 false,执行 else 块
if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
// ...
} else {
mAdapterUpdateDuringMeasure = true;
requestLayout();
}
}
在保存了一些信息后,还是进行视图重绘。来到了 layout 过程后,还是以 LinearLayoutManager 为例,这次先看下布局过程的 step1,也就是 dispatchLayoutStep1 方法:
RecyclerView#dispatchLayoutStep1
1 | private void dispatchLayoutStep1() { |
主要看 processAdapterUpdatesAndSetAnimationFlags 方法,从名字也可以看出,它负责更新 adapter 的信息:1
2
3
4
5
6
7
8
9
10
11 private void processAdapterUpdatesAndSetAnimationFlags() {
// ...
if (predictiveItemAnimationsEnabled()) {
mAdapterHelper.preProcess();
} else {
mAdapterHelper.consumeUpdatesInOnePass();
}
// ...
}
这里借助了 mAdapterHelper,它最终又通过接口回调(回调了 markViewHoldersUpdated 方法)调用了 RecyclerView 的 viewRangeUpdate 方法:1
2
3
4
5
6
7
8
9
10
11
12
13void viewRangeUpdate(int positionStart, int itemCount, Object payload) {
// ...
for (int i = 0; i < childCount; i++) {
// ...
if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
// (1)
holder.addFlags(ViewHolder.FLAG_UPDATE);
// ...
}
}
}
该方法就是遍历所有子 View,找到所有发生了改变的子 View,进行相关操作。这里重点看注释(1),为改变的 ViewHolder 添加了 FLAG_UPDATE 标记。先记住这点,在后面会用到。
接下来看 onLayoutChildren 方法,和 notifyDataSetChanged 一样,主要的不同之处也是在于 detachAndScrapAttachedViews 方法,该方法遍历子 View,调用 scrapOrRecycleView 方法,下面看一下该方法:
LayoutManager#scrapOrRecycleView
1 | private void scrapOrRecycleView(Recycler recycler, int index, View view) { |
这里就和 notifyDataSetChanged 时不一样了,由于在视图重绘前没有给 ViewHolder 添加 FLAG_INVALID 标记,这次进入的是 else 块。
首先将 View 从 RecyclerView 中 detach 掉(而不是 remove 掉)。然后在回收时,调用的是 Recycler 的 scrapView 方法。该方法在前面也分析过了,这里再看一次:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
// 满足这几个条件中的一个就可以进入 if 循环
// 1. ViewHolder 设置了 FLAG_REMOVED 或 FLAG_INVALID
// 2. ViewHolder 没有设置 FLAG_UPDATE
// 3. 没有设置动画或者动画可以重用该 ViewHolder
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
// ...
mAttachedScrap.add(holder);
}
// 不满足上述任意一个条件时,将 View 缓存到 mChangedScrap 中
else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
重点看判断里面的条件 2,从前面的分析可以得知,对于发生改变的 ViewHolder,给它设置了 FLAG_UPDATE,所以它现在三个条件都不满足,进入 else 块,而对于其他的 ViewHolder,由于没有设置 FLAG_UPDATE,所以满足条件 2,进入 if 循环。
所以通过 notifyItemChanged 方法更新列表时,发生了改变的子 View 将被缓存到 ChangedScrap 中,而没有发生改变的子 View 则缓存到 AttachedScrap 中,之后通过填充布局的时候对于不同 item 就可以从相应的 Scrap 缓存中得到子 View。
另外,Scrap 缓存只作用于布局阶段,在 layout 的 step3 中将会清空 mAttachedScrap 和 mChangedScrap。
其实还有一个常见的场景是滑动操作,滑动出屏幕的子 View 将会缓存到 mCachedView,不过这里就不详细说了,在之后会在其他文章专门分析滑动这块。
后记
本文围绕 Recycler 展开叙述,重点是要通过它的几个成员变量了解它的缓存机制,四级缓存分别是什么,是在何时调用的,各自起到的作用,不同场景下使用哪种缓存等。
Recycler 和 LayoutManager 的布局以及动画都有联系,例如 LayoutManager 负责布局,它决定获取子 View 和回收子 View 的时机,具体的工作就交由 Recycler 负责。这些会在之后对 RecyclerView 的其他方面作分析时进行更详细的说明。