前言
- 上一篇文章RecyclerView之布局设计
RecyclerView,见名之义,这个View代表了可循环使用的视图集合控件,封装了View的缓存逻辑判断,RecyclerView的基本单元是ViewHolder,里面有一个itemView代表了视图上的子View,所以RecyclerView的缓存基本单元也是ViewHolder。本文将从源码的角度来讲解RecyclerView的缓存设计。
本文相关源码基于Android8.0,相关源码位置如下:
frameworks/support/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
frameworks/support/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
Recycler介绍
这里首先介绍一下Recycler,它定义在RecyclerView中,如下:
1 | public final class Recycler { |
Recycler是RecyclerView的核心类,是RecyclerView的缓存实现类,它有着四级缓存:
1、mAttachedScrap
屏幕内缓存, 当我们调用notifiXX函数重新布局时,在布局之前,LayoutManager会调用detachAndScrapAttachedViews(recycler)把在RecyclerView中显示的ViewHolder一个个的剥离下来,然后缓存在mAttachedScrap中,等布局时会先从mAttachedScrap查找,再把ViewHolder一个个的放回RecyclerView原位中去,mAttachedScrap只是单纯的保存从RecyclerView中剥离的ViewHolder,再重新放回RecyclerView中去,如果放回后还有剩余的ViewHolder没有参加新布局,会从mAttachedScrap移到mCachedViews中。2、mCachedViews
在RecyclerView滚动时,对于那些不在RecyclerView中显示的ViewHolder,LayoutManager会调用removeAndRecycleAllViews(recycler)把这些已经移除的ViewHolder缓存在mCacheViews中,它的默认大小是2,当它满了的时候,就会利用先进先出原则,把老的ViewHolder移到mRecyclerPool中,mCachedViews它只是缓存最新被移除出屏幕的ViewHolder。3、mViewCacheExtension
自定义缓存实现,一般而言,我们不会自定义缓存实现,使用Recycler提供的3级缓存足够。4、mRecyclerPool
缓存池,通过前面1、2可以知道,真正废弃的ViewHolder最终移到mRecyclerPool,当我们向RecyclerView申请一个HolderView来使用的时,如果在mAttachedScrap、mCachedViews匹配不到,即使他们中有ViewHolder也不会返回给我们使用,而是会到mRecyclerPool中去拿一个废弃的ViewHolder返回。mRecyclerPool内部维护了一个SparseArray,在mRecyclerPool中会根据每个ViewType把ViewHolder分别存储在不同的列表中,每个ViewType默认缓存5个ViewHolder,而且RecyclerViewPool也可以是多个RecyclerView之间的ViewHolder的缓存池,只要通过RecyclerView.setRecycledViewPool(RecycledViewPool)设置同一个RecycledViewPool,设置时,不需要自己去new 一个 RecyclerViewPool,每个RecyclerView默认都有一个RecyclerViewPool,只需要通过mRecyclerView.getRecycledViewPool()获取。RecyclerViewPool大概结构如下:
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
33
34
35
36public 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;
//...
}
//SparseArray的key为type,value为ScrapData,ScrapData中包含ViewHolder列表
SparseArray<ScrapData> mScrap = new SparseArray<>();
//...
//根据type从缓存池中获取一个ViewHolder
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
return scrapHeap.remove(scrapHeap.size() - 1);
}
return null;
}
//把一个ViewHolder放入缓存池中缓存
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;
}
//...
scrap.resetInternal();
scrapHeap.add(scrap);
}
}所以我们从Recycler中获取一个ViewHolder时,是这样的顺序:mAttachedScrap -> mCachedViews -> mViewCacheExtension -> mRecyclerPool,当上述步骤都找不到了,就会调用Adapter的creat函数创建一个ViewHolder。那这里为什么省略mChangedScrap不讲呢?因为mChangedScrap是跟RecyclerView的预布局有关,缓存着RecyclerView中数据改变过的ViewHolder,而预布局默认为false,一般是RecyclerView执行动画时才会为true,我们上一篇文章也没有讨论执行动画的时候的布局过程,所以这里就不分析mChangedScrap。
Recycler.getViewForPosition()
在上篇文章中,提到在layoutChunk函数中,首先会调用LayoutState对象的next函数获取到一个itemView,然后布局这个itemView,我们来看LayoutState的next函数相关实现:
1 | View next(RecyclerView.Recycler recycler) { |
上述代码实际是调用RecyclerView.Recycler对象的getViewForPosition方法获取itemView,而该函数最终会获取一个ViewHolder,从而返回ViewHolder中的itemView,我们来看该函数相关调用和实现:
1 | public View getViewForPosition(int position) { |
Recycler的getViewForPosition方法最终会调用到tryGetViewHolderForPositionByDeadline方法,tryGetViewHolderForPositionByDeadline方法的意图是通过给定的position从Recycler的scrap, cache,RecycledViewPool获取一个ViewHolder或者通过Adapter直接创建一个ViewHolder。我们来看tryGetViewHolderForPositionByDeadline方法相关源码:
1 | //参数解释: |
看起来函数很长但是步骤还是很清晰的,我们把它分为注释1、2、3、4、5、6来看:
1、调用getScrapOrHiddenOrCachedHolderForPosition()
注释1中通过position从scrap或hidden或cache中找ViewHolder,我们来看getScrapOrHiddenOrCachedHolderForPosition方法的关键源码:
1 | ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) { |
可以看到注释1的第一次查找,里面分为3步:
- 1.1、从mAttachedScrap找。
- 1.2、如果上一步没有得到合适的缓存,从HiddenViews找。
- 1.3、如果上一步没有得到合适的缓存,从mCachedViews找。
从上面3个步骤之一找到,就返回ViewHolder,然后检查ViewHolder的有效性,如果无效,则从mAttachedScrap中移除,并加入到mCacheViews或者mRecyclerPool中,并且将ViewHolder置为null,走到下一步。
2、调用getScrapOrCachedViewForId()
下一步就是注释2,如果我们通过Adapter.setHasStableIds(boolean)设置为true,就会进入,里面根据ViewHolder的type和id从scrap或cached列表查找ViewHolder,我们来看一下相关源码该方法的相关源码:
1 | ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) { |
可以看到注释2的第二次查找,里面分为2步:
- 2.1、从mAttachedScrap找。
- 2.2、如果上一步没有得到合适的缓存,从mCachedViews找。
第二次查找跟第一次不同的是,它是通过Adapter.getItemId(position)获得该位置ViewHolder的id,来查找ViewHolder,我们可以重写Adapter.getItemId(position)返回每个position的ViewHolder的id,默认返回RecyclerView.NO_ID。从上面2个步骤之一找到,就返回ViewHolder,如果找不到就进入下一步。
3、从ViewCacheExtension中找
注释3的第三次查找是从自定义缓存中查找,这个没什么好说,可以直接到下一步。
4、从RecyclerViewRool中找
下一步就是第4次查找,从RecyclerdViewPool中查找,可以看到这里先使用getRecyclerViewPool获得Recycler中的RecyclerViewPool,然后调用RecyclerViewPool的getRecycledView(type)根据type获取一个ViewHolder,我们来看该方法的源码:
1 | public ViewHolder getRecycledView(int viewType) { |
mScrap是SparseArray类型,它会根据type把ViewHolder存放在不同ScrapData中,ScrapData中有一个mScrapHeap,是ArrayList类型,它会存放RecyclerViewPool中放进来的ViewHolder。所以上面这个方法首先会根据type取出ScrapData,然后取出mScrapHeap,如果mScrapHeap有元素,就返回并删除,然后重置这个ViewHolder让它复用,如果没有就进入下一步。
5、调用Adapter的createViewHolder()
既然缓存中没有就创建一个,该方法的相关源码如下:
1 | public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) { |
可以看到,调用了我们熟悉的onCreateViewHolder方法,该方法就是用来创建ViewHolder。
到这里,经过tryGetViewHolderForPositionByDeadline方法中的注释1、2、3、4、5步骤之一拿到了ViewHolder,接下来就是看是否需要调用Adapter的OnBindViewHolder方法绑定ViewHolder。
6、根据情况调用Adapter的OnBindViewHolder()
从上面知道当缓存中不能提供ViewHolder就会调用adapter的onCreateViewHolder创建一个,那么我们同样熟悉的OnBindViewHolder方法是什么时候执行的呢?如下:
1 | ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { |
holder.isBound()、holder.needsUpdate() 、holder.isInvalid()方法中会分别判断ViewHolder中有没有设置FLAG_BOUND标志位、FLAG_UPDATE标志位、FLAG_INVALID标志位,只要满足3种情况之一,就会调用Adapter的OnBindViewHolder方法绑定数据,这3种情况的解释如下:
- 1、没有设置FLAG_BOUND标志位:它表示ViewHolder没有调用过OnBindViewHolder方法,一般是调用Adapter的OnCreateViewHolder方法创建的ViewHolder会出现这种情况;
- 2、设置了FLAG_UPDATE标志位:它表示ViewHolder需要更新,一般是调用了Adapter的定向更新的相关方法或者ViewHolder是从RecycledViewPool中取出的就会出现这种情况;
- 3、设置了FLAG_INVALID标志位:它表示ViewHolder是无效的,一般是从mAttachedScrap或mCacheViews中取出ViewHolder后,发现它满足被移除或者position越界了等不合法的条件,就会把取出ViewHolder设置FLAG_INVALID标志位,标志无效,然后调用recycleViewHolderInternal方法把它放入mCacheViews或RecycledViewPool中,在recycleViewHolderInternal方法中ViewHolder首先会被尝试放入mCacheViews(默认大小为2)中,如果满了,就会利用先进先出原则,把老的ViewHolder移到mRecyclerPool中。
bind方法是用来绑定数据,对于从mAttachedScrap中拿出来的ViewHolder是不用重新bind的,而对于从mRecyclerPool拿出和通过Create方法创建的ViewHolder是需要重新bind的,而对于从mCacheViews中拿出的ViewHolder有可能会被bind,当调用getScrapOrHiddenOrCachedHolderForPosition方法根据position获取ViewHolder时,如果这个ViewHoler是从mCacheViews中取出的,说明满足有效的、positioin匹配这两种情况,如果这个ViewHolder同时是合法的,那么这个ViewHolder不需要重新bind,而如果是不合法的,就会标志无效,再次放入mCacheViews中(有可能会移动到mRecyclerPool),等待调用getScrapOrCachedViewForId方法根据type和id从mCacheViews再次获取这个已经被标记为无效的ViewHolder,如果这个无效的ViewHolder的type和id都匹配的话,就会获取这个无效的ViewHolder,而此时这个ViewHolder是需要重新bind的。
从前面的分析来看,mAttachedScrap和mCacheViews都是position匹配或者type和id匹配才会命中返回ViewHolder,而mRecyclerPool则没有这些限制,只要mRecyclerPool中相应type类型的ViewHolder缓存有,就会命中返回ViewHolder,且优先级mAttachedScrap > mCacheViews > mRecyclerPool,通过以下3个场景,加深大家理解mAttachedScrap、mCacheViews、mRecyclerPool的作用:
1、当RecyclerView列表上下滑动时,屏幕内的ViewHolder会被缓存到mAttachedScrap中,在屏幕内改变位置的ViewHolder复位后,很快会从mAttachedScrap复用到原位置上;
2、当RecyclerView列表向上滑动,列表顶部有ViewHolder滑出屏幕,滑出屏幕的ViewHolder会被缓存到mCacheViews中,当列表向下滑动复位时,滑出屏幕的ViewHolder很快从mCacheViews复用到原位置上;
3、当RecyclerView列表向上滑动,列表顶部有ViewHolder滑出屏幕,滑出屏幕的ViewHolder会被缓存到mCacheViews中,多余的会移动到mRecyclerPool中,列表底部有空余的ViewHolder位置,这时会从mRecyclerPool取出ViewHolder复用,填充底部空余的ViewHolder位置。
总结
本文中源码角度简单的分析RecyclerView布局一个itemView时是怎样通过Recycler来获取一个ViewHolder,从而获取itemView,如图:
准确的来说,Recycler是RecyclerView的itemView的提供者和管理者,它在内部封装了RecyclerView的缓存设计实现,在RecyclerView中有着四级缓存:AttachedScrap,mCacheViews,ViewCacheExtension,RecycledViewPool,正因为这样RecyclerView在使用的时候效率更好。
参考文章: