“翠竹清流,婆娑鹤鸣。我喜欢的不止那如洗碧空,斑驳宁静。”
前言
这篇拖了好久其实,本来早一周前就可以发了
不过这篇本身比较费时费力是一回事,前几天还感冒了
于是一直处于流鼻涕-吃药-睡觉-吃饭-吃药睡觉
的状态,中间还偷懒玩游戏。
一起玩哈利波特的小伙伴可以找我一起,嘻嘻
哦对了, 为了不让文档加载太慢,我把文中的图片都压缩了,如果想要原图可以联系我。
又见 RecyclerView:缓存原理
之前写了下 RecyclerView 的基本用法,传送门:初识 RecyclerView:基本使用
这里打算顺便学习下 RecyclerView缓存和复用的原理,你也就顺便看看。
下面内容应该会涉及到源码,因为篇幅问题不会给的很全,最好还是自己动手,对着源码翻翻看看。
对于缓存和复用,我们关心的无非就是存到哪、存什么、什么时候存的问题,搞懂这三个问题其实就差不多了。
四级缓存
先来解决存到哪的问题,RecyclerView 内部进行了四种缓存的分级,了解四种缓存是学习 RecyclerView 缓存复用机制的基础。
缓存 | 作用 |
---|---|
一级缓存:屏幕内缓存(mAttachedScrap、mChangedScrap) | mAttachedScrap 缓存未与 RecyclerView 分离的 ViewHolder mChangedScrap 缓存发生改变的 ViewHolder,需调用 onBindViewHolder() 重新绑定数据。 |
二级缓存:屏幕外缓存(mCachedViews) | 缓存滑倒屏幕外的 ViewHolder,默认容量为 2。若容量已满,会把旧 ViewHolder 移入缓存池 RecycledViewPool。(队列结构,先进先出) |
三级缓存:自定义缓存(ViewCacheExtension) | 用户自定义的扩展缓存,需要用户自己管理 View 的创建和缓存。 |
四级缓存:缓存池(RecycledViewPool) | 根据 ViewType 缓存 ViewHolder,每个 ViewType 默认最多缓存 5 个。用 SparseArray 实现。 |
一级缓存为什么要有mAttachedScrap
和mChangedScrap
两种缓存呢?看这两种缓存的名字就知道我们要先理解“Attached”和“Changed”两种状态的区别。
所谓“Attached”状态,指的是 ViewHolder 和 RecyclerView 存在联系的状态,说明这个 ViewHolder 明确要被显示到 RecyclerView 上。
那“Changed”状态则相反,指这个 ViewHolder 可能不需要被显示。比如我这个 Item 可能要被删掉,那么其对应的 ViewHolder 就不应该被显示;比如这个 Item 的数据需要更新,那么 ViewHolder 就应该先更新数据,再进行显示。
因此,每当 Item 中有数据更新时,重新绘制列表前,会先清除所有 Item。然后把没有变化的 Item 放入mAttachedScrap中,需要被更新的 Item 放入mChangedScrap中。
似乎看起来有些麻烦,为什么不直接修改 Item 反而要清除所有 Item 再绘制呢?其实这是一种责任分离机制。在绘制布局的时候,发挥作用的是LayoutManager,而进行缓存的存取操作是由Recycler控制的。采用这种方式控制 View,LayoutManager能够把所有 View 全交给Recycler,让Recycler判断 View 是否更新,需要被放进哪个缓存中,实现缓存都归 Recycler 管的目的。有得必有失嘛。
不过有时候 RecycerView 的缓存机制又被称为三级缓存。这种叫法将mCachedViews作为一级缓存,认为两个 Scrap 只是临时保存数据的容器而不是缓存。两种叫法都可以,挑你喜欢的记。因为喜欢四级缓存的概念所以下面都用四级缓存。
现在我们对四种缓存有了概念,就可以跟着源码学习了。我们把 RecyclerView 的缓存机制分为两部分。缓存就是把 Item 存入缓存的过程,复用就是把 Item 从缓存取出的过程。
缓存机制
我们从 RecyclerView 的布局载入入手,毕竟这个时候是一个从无到有的过程。因此这时候缓存区里的东西都是空的,讲道理应该只能进行缓存不能复用,再加上从逻辑上讲也得现有缓存才能复用,所以我们就先看看 RecyclerView 缓存是怎么实现的。
1. LayoutManager 起手:获取 View
因为 RecyclerView 的布局是由LayoutManager实现的,所以在RecyclerView.LayoutManager
里,我们找到了一个detachAndScrapAttachedViews()
方法,这个方法循环调用 scrapOrRecycleView()
方法,将当前 LayoutManager 下所有的 View 依次取出,判断 View 应该“scrap”or“recycle”
//in RecyclerView.LayoutManager
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
······//Log信息
if (viewHolder.isInvalid() && !viewHolder.isRemoved() //如果ViewHolder失效了但没有被删除(屏幕外)
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder); //recycle view 加入其他缓存
} else { //如果ViewHolder没有失效
detachViewAt(index);
recycler.scrapView(view); //scrap view 加入一级缓存
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
每次调用scrapOrRecycleView()
方法时,Recycler 都对 ViewHolder 进行一个判断,如果这个 ViewHolder 失效了但没有被删除(比如滑到屏幕外),则调用recycleViewHolderInternal()
方法;反之,则调用scrapView()
。我们简单理解为没有显示在屏幕上的 ViewHolder 被扔进recycleViewHolderInternal()
,而即将显示在屏幕上的则被扔进scrapView()
。由于此时传入一个Recycler作为参数,所以这时开始由Recycler接手,判断 ViewHolder 应该去哪个缓存。
2. 将 View 加入一级缓存:scrapView()
recycleViewHolderInternal()
和scrapView()
都是RecyclerView.Recycler中的方法,这时候开始由Recycler控制缓存。因为刚开始载入布局还没有滑动操作、不需要进行更新操作,大部分都不会失效,所以基本上此时的 View 都进入scrapView()
操作,我们来看看这个方法做了什么
//in RecyclerView.Recycler
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { //没被删除、没更新等
// ~ 报错信息
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder); //加入mAttachedScrap
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder); //加入mChangedScrap
}
}
很显然这里将 ViewHolder 进行了一个判断,如果 ViewHolder 没被删除、没更新等操作,则这个 ViewHolder 通过mAttachedScrap.add()
被加入mAttachedScrap 缓存中;反之,则通过mChangedScrap.add()
加入mChangedScrap 缓存中。他们都是一级缓存,因此scrapView()
负责将 View 放入一级缓存中。
3. 将 View 加入二级/四级缓存:recycleViewHolderInternal()
在LayoutManager的scrapOrRecycleView()
中,我们用scrapView()
把 ViewHolder 加入一级缓存,现在剩下的 ViewHolder 就要加入其他的缓存了,用的方法就是recycleViewHolderInternal()
因为这个方法代码略长,这里节选了关键代码
//RecyclerView.Recycler.recycleViewHolderInternal()
······
if (mViewCacheMax > 0 //如果mCachedViews默认大小大于0并且ViewHolder没啥问题(被移除、更新等)
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
//这里判断mCachedViews是否满了,满了调用recycleCachedViewAt(0),移出一个View,腾出空间
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0); //mCachedViews满了,移出一个View,腾出空间
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
······ // when adding the view, skip past most recently prefetched views
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
······
关键代码一开始进行一个判断,判断里的mViewCacheMax
是Recycler的一个属性,表示缓存大小。通常mViewCacheMax = DEFAULT_CACHE_SIZE
,而默认DEFAULT_CACHE_SIZE = 2
,他表示二级缓存 mCachedViews的默认大小,默认缓存两个 ViewHolder。
总之就是,一开始判断mCachedViews的默认大小是否为 0,ViewHolder 是否有被移除、更新等问题。不过一般情况下mCachedViews的默认大小为 2,送来的 ViewHolder 也没啥问题,因此这个判断通常都是true
然后再判断,mCachedViews有没有满。如果满了,就调用recycleCachedViewAt(0)
,从中移出一个 View,腾出空间。如果没满,则调用mCachedViews.add
直接加入mCachedViews。
如果没有设置二级缓存mCachedViews,或者缓存过程中出现意外,导致没有成功执行到最后的cached = true
(!cached
),那么就用addViewHolderToRecycledViewPool()
直接扔到缓存池里。
4. mCachedViews 已满:recycleCachedViewAt()
Recycler 执行recycleViewHolderInternal()
的时候,先尝试将 ViewHolder 放入二级缓存 mCachedViews,如果他满了,则会执行recycleCachedViewAt(0)
,尝试将 ViewHodler 到四级缓存,缓存池 RecycledViewPool中
//RecyclerView.Recycler
void recycleCachedViewAt(int cachedViewIndex) {
······//一些Log信息
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
addViewHolderToRecycledViewPool(viewHolder, true); //将ViewHolder放入RecycledViewPool
mCachedViews.remove(cachedViewIndex); //将ViewHolder从mCachedViews中删除
}
在这个方法中,我们将获取到的 viewHolder 利用addViewHolderToRecycledViewPool()
方法移动到四级缓存 RecycledViewPool中。
然后利用remove()
方法将mCachedViews中cachedViewIndex
位置的 ViewHolder 删除。在recycleViewHolderInternal()
中调用recycleCachedViewAt()
的时候传入的一定是index=0
,那为什么一定是index=0
的 ViewHolder 呢?
因为mCachedViews采用的是队列的逻辑结构,也就是先进先出(FIFO)策略,因为 RecyclerView 默认缓存中新的 ViewHolder 比旧的更容易被复用。就好比我们向下滑动屏幕,头两个 Item(Item1 和 Item2)被滑到屏幕外了,于是按照顺序,Item1 和 Item2 依次被缓存入mCachedViews。如果我们这时候重新往上滑,则先进入屏幕的是 Item2,也就是 Item2 需要从缓存中取出被复用。
再加上mCachedViews是一个ArrayList
的结构,其remove()
操作会将被删除数据后面的数据左移。因此就能保证mCachedViews中index=0
的位置是最先进入缓存的 ViewHolder。
5. 将 View 加入四级缓存 RecycledViewPool:putRecycledView()
Recycler的addViewHolderToRecycledViewPool()
方法用来把 ViewHolder 放入四级缓存 RecycledViewPool,关键代码其实就一行:
//addViewHolderToRecycledViewPool()
getRecycledViewPool().putRecycledView(holder);
getRecycledViewPool()
得到一个RecycledViewPool对象,然后把 ViewHolder 加到里面去。
缓存池 RecycledViewPool 的结构
在继续探究之前,我们先来了解一下缓存池 RecycledViewPool的结构。
RecycledViewPool是 RecyclerView 的一个内部类,代表我们的四级缓存。其拥有一个属性DEFAULT_MAX_SCRAP = 5
表示其默认缓存大小。
事实上,RecycledViewPool中真正存储 View 的结构是SparseArray<ScrapData> mScrap
,泛型为ScrapData的SparseArray类型。
ScrapData是RecycledViewPool的一个内部类,他的主要结构是ArrayList<ViewHolder> mScrapHeap
,这个mScrapHeap
就是缓存池中保存我们 ViewHolder 的容器了。
简单介绍下SparseArray,他是 Android 独有的结构。和 HashMap 类似,采用键值对的形式进行存储,不同的是他的键(Key)为 int 类型。从而在存储时不用对 Key 进行自动装箱的操作,实现性能的提升。由于 SparseArray 采用二分搜索,因此其始终都保持有序。
HashMap 采用数组+链表的结构,而 SparseArray 则是纯数组结构。其包含两个数组,分别表示 key 和 values,两组数据对应。
因为ScrapData的主体是mScrapHeap,RecycledViewPool的主体是mScrap,我们可以简单地认为ScrapData 就是 mScrapHeap,简单地认为四级缓存的RecycledViewPool是一个mScrap(SparseArray结构)
而这个SparseArray的Key就是我们所说的viewType
。是不是听起来很眼熟,在使用 RecyclerView 的时候,如果我们需要使用多种 Item,则要对每个 Item 进行标识,这个标识就是viewType
。这个SparseArray的Value则是一个装 ViewHolder 的数组(mScrapHeap)。结构示意图如下(我用 PS 画了好久。。)
6. RecycledViewPool 的存储细节
这下事情似乎变得明朗了,基于 SparseArray 的结构,RecycledViewPool拿到一个 ViewHolder,把他存到其对应viewType
后的mScrapHeap中
讲道理,RecycledViewPool的大小应该由 Item 的种类决定。因为有几种 Item 就有几个viewType
,viewType
作为Key,决定了RecycledViewPool这个数组的大小。那我们之前说的默认缓存大小DEFAULT_MAX_SCRAP = 5
又是什么呢?自然是每个 viewType 之后,作为Value的mScrapHeap的大小,即对于一个 viewType,能装其五个对应的 ViewHolder。
这下看putRecycledView()
中的代码就清晰了,这个方法将 ViewHolder 放入缓存池RecycledViewPool。
//RecyclerView.RecycledViewPoor
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); // ~ 加入缓存池
}
我们先getScrapDataForType(viewType)
根据viewType
得到对应的scrapData(mScrapHeap)
,其中就是缓存池中的 ViewHolder 了,也就是说此时我们已经拿到了缓存的ViewHolder了。
我们现在是要把传进来的ViewHolder(scrap
)尝试放入RecycledViewPool嘛,所要先进行一个大小判断,如果缓存中的ViewHolder数量(scrapHeap.size()
)大于设定的最大数量(mMaxScrap
),就说明缓存满了,那就不用放了,直接 return,丢弃这个 ViewHolder。
如果没满,那我们先调用resetInternal()
将这个ViewHolder进行重置,然后 add 进RecycledViewPool缓存。所谓重置,是指将这个 ViewHolder 的一些标识初始化,比如消除ItemId
、Position
等设置,我们可以理解为抹除这个 ViewHolder 所绑定的数据,只留下一个空的框架。
这就解释了为什么当mCachedView满了的时候,我们要宁愿要把其中旧的 ViewHolder 移动到RecycledViewPool,也要把新的 ViewHolder 放入mCachedView。但是当RecycledViewPool满了,我们直接把手上的 ViewHolder 丢弃。
mCachedView中的 ViewHolder 是带有其数据的,因此其进行复用的时候能更高效。而RecycledViewPool中的 ViewHolder 是没有数据空有框架的,因此每次复用的时候,还要调用onBindViewHolder()
为 ViewHolder 绑定数据。
或许有人要问,既然RecycledViewPool是空的框架,里面所有的 ViewHolder 都是一样的,我为什么不存一个而是存五个呢?实际上这五个默认值是官方为了适应大多数场景而设计的。比如我们的 RecyclerView 有 3 列,那么每次滑入滑出就有 3 个 ViewHolder 受到影响,这种情况下如果只有一个缓存显然是不够的。
如果缓存太多,则会占用太多内存,如果缓存太少,则会频繁调用onCreateViewHolder()
创建新的 ViewHolder,造成更多的耗时。达到一个平衡点才是我们追求的优化关键。
总结
注意这里的入口是布局开始的时候,不过实际上缓存进行的过程不止这个地方。包括滑动列表啦之类的都会导致缓存复用的操作,这里只是举个例子而已。
RecyclerView.LayoutManager -> detachAndScrapAttachedViews()
-> scrapOrRecycleView()
-> RecyclerView.Recycler 接手 -> recycleViewHolderInternal()
/ scrapView()
recycleViewHolderInternal()
:
没有 mCachedViews -> addViewHolderToRecycledViewPool()
。
mCachedViews没有满 -> mCachedViews.add()
mCachedViews满了 -> recycleCachedViewAt(0)
-> addViewHolderToRecycledViewPool()
(getRecycledViewPool().putRecycledView()
-> scrap.resetIntrenal()
-> scrapHeap.add()
) -> mCachedViews.remove()
scrapView()
-> 没问题的 ViewHolder 加入mAttachedScrap(mAttachedScrap.add()
),反之加入mChangedScrap(mChangedScrap.add()
)
复用机制
1. ReyclerView:滑动产生复用
复用指的是从缓存中取出 ViewHolder 的过程,因此我们从滑动屏幕的操作入手,找找复用的入口。
于是来到RecyclerView中的onTouchEvent()
方法,在ACTION_MOVE
操作下发现scrollByInternal()
方法,其中的scrollStep()
对滑动操作进行了处理:
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
······
int consumedX = 0;
int consumedY = 0;
if (dx != 0) {
consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
}
if (dy != 0) {
consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
}
······
}
这里的scrollHorizontallyBy()
和scrollVerticallyBy()
分别标识横向滑动和纵向滑动,他们都由LayoutManager实现。
2. 从 LayoutManager 到 Recycler
于是我们跳转到LinearLayoutManager查看此方法。
//LinearLayoutManager
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == VERTICAL) {
return 0;
}
return scrollBy(dx, recycler, state);
}
显然重要的是scrollBy()
方法,这个方法比较长,之后七弯八拐地有点绕,这里简单指个路。
scrollBy() -> fill() -> layoutChunk() -> next()
。 之后来到Recycler的工作区,开始调用Recycler的方法。next() -> getViewForPosition() -> tryGetViewHolderForPositionByDeadline()
,这个名字很长的方法的返回值终于是我们的ViewHolder了。
但是这个名字很长的方法真的很长,所以这里简单介绍下。
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
······// ~ 报错信息
ViewHolder holder = null;
// ~ 先从mChangedScrap里拿ViewHolder
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// ~ 没拿到ViewHolder则holder仍为null,所以从mAttachedScrap和mCachedViews拿(根据Position)
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
······
}
// ~ 还还还没拿到
if (holder == null) {
·····
// ~ 尝试从mAttachedScrap和mCachedViews里根据id拿到ViewHolder
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
// ~ 从三级缓存mViewCacheExtension中拿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);
if (view != null) {
holder = getChildViewHolder(view);
······// ~ 报错信息
}
}
// ~ 还是没拿到,从四级缓存RecycledViewPool中拿
if (holder == null) { // fallback to pool
······// ~ 报错信息
holder = getRecycledViewPool().getRecycledView(type);
······
}
// ~ 四级缓存里都没有,只能创建
if (holder == null) {
long start = getNanoTime();
······
holder = mAdapter.createViewHolder(RecyclerView.this, type);
······
}
// This is very ugly but the only place we can grab this information
// before the View is rebound and returned to the LayoutManager for post layout ops.
// We don't need this in pre-layout since the VH is not updated by the LM.
······// ~ 收集信息
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
······// ~ 报错信息
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
······// ~ 设置布局信息LayoutParams
return holder;
}
因为mChangedScrap
中装的是修改过,等着被显示的 Item,所以Recycler会先判断其中是否有 ViewHolder(isPreLayout()
)。如果有就调用getChangedScrapViewForPosition()
去其中获取ViewHolder。
如果没有,则此时仍然holder=null
,所以再从mAttachedScrap
和mCachedViews
查找 ViewHolder(根据 Position),就尝试根据 id 去拿ViewHolder,即getScrapOrCachedViewForId()
如果又没有,那么就从三级缓存mViewCacheExtension
中寻找,即getViewForPositionAndType()
不过因为偷懒这个缓存是自定义的,所以就不过多讲解了。
如果还是没有,只能去四级缓存RecycledViewPool
中寻找了。先用getRecycledViewPool()
得到当前的 RecycledViewPool 对象,再调用getRecycledView()
去获取 ViewHolder
如果叕(zhuó)真的没有,那就说明缓存里是真的没了,老老实实调用createViewHolder()
创建 ViewHolder 吧。
最后还会调用tryBindViewHolderByDeadline()
为 VIewHolder 绑定数据。不过由于之前寻找 ViewHolder 的时候,返回这个 ViewHolder 之前都会为其打上标记,所以在绑定数据前会进行一个判定。只有来自四级缓存RecycledViewPool
和新建的 ViewHolder 才会去绑定。而其他缓存里的 ViewHolder 在存的时候是带数据一起存的,所以不需要再额外绑定了。
接下来就一步步看看Recycler是怎么找 ViewHolder 的吧。
2.1. 从 mChangedScrap 获取 ViewHolder:getChangedScrapViewForPosition()
mChangedScrap
中装的是修改过,等着被显示的 Item,所以Recycler首先从他里面获取 ViewHolder,称其为预布局 PreLayout。于是进入getChangedScrapViewForPosition()
方法。
方法中先判断mChangedScrap
是否存在以及其中是否有 ViewHolder。如果不存在或者没有 ViewHolder 则无法从中取出东西,所以直接return
反之,如果有 ViewHolder,那么我们就可以直接从中拿出,然后打上一个来自一级缓存的标签。这里实际上分了两步走,分别根据 Position 和 Id 来寻找 ViewHolder。当然如果都找不到就返回 null 了,我们需要进一步努力。
ViewHolder getChangedScrapViewForPosition(int position) {
// If pre-layout, check the changed scrap for an exact match.
final int changedScrapSize;
if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
return null; //判断mChangedScrap是否存在以及其中是否有ViewHolder
}
// find by position
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
// ~ 打上标签,,标识其来自一级缓存
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
// find by id
if (mAdapter.hasStableIds()) {
······
// ~ 打上标签,,标识其来自一级缓存
if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
return null;
}
2.2. 从 mAttachedScrap 和 mCachedViews 获取 ViewHolder:getScrapOrHiddenOrCachedHolderForPosition()
我们想从mChangedScrap
中获取 ViewHolder 失败了,于是转眼来到getScrapOrHiddenOrCachedHolderForPosition()
,开始针对同是一级缓存的mAttachedScrap
和二级缓存mCachedViews
虽然方法名字很长,但整体还是比较简单的。
先从mAttachedScrap
获取ViewHolder,当这个 ViewHolder 有效且可见,位置还对的上的时候就返回。
如果没有拿到就从从mCachedViews
获取ViewHolder。当然如果都没拿到就返回null
返回前都打上Flag
标记
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// ~ 先从mAttachedScrap获取ViewHolder
// Try first for an exact, non-invalid match from scrap.
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
······
// ~ 然后从mCachedViews获取ViewHolder(源码注释称其为一级(first-level)缓存)
// Search in our first-level recycled view cache.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
// invalid view holders may be in cache if adapter has stable ids as they can be
// retrieved via getScrapOrCachedViewForId
if (!holder.isInvalid() && holder.getLayoutPosition() == position
&& !holder.isAttachedToTransitionOverlay()) {
······
return holder;
}
}
return null;
}
2.3. 根据 Id 获取 View:getScrapOrCachedViewForId()
能来到这里,说明我们从mChangedScrap
中没拿到 View,而且根据position
并不能从mAttachedScrap
和mCachedViews
中拿到 View。于是尝试根据 Id 拿到 View,当然还是mAttachedScrap
和mCachedViews
这两个缓存中。
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
// ~ 先从mAttachedScrap中拿
// Look in our attached views first
final int count = mAttachedScrap.size();
for (int i = count - 1; i >= 0; i--) {
final ViewHolder holder = mAttachedScrap.get(i);
if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
if (type == holder.getItemViewType()) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
······
return holder; // ~ 拿到了就返回
}
······
}
}
// ~ 再从mCachedViews中拿
// Search the first-level cache
final int cacheSize = mCachedViews.size();
for (int i = cacheSize - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
if (holder.getItemId() == id && !holder.isAttachedToTransitionOverlay()) {
if (type == holder.getItemViewType()) {
······
return holder; // ~ 拿到了就返回
}
······
}
}
return null; // ~ 没拿到返回空
}
简单来说,和上面根据Position
拿 View 的方法差不多,都是在mAttachedScrap
和mCachedViews
这两个缓存中找 View,找到就返回,找不到就返回null
,唯一的区别就在于这里找 View 的依据是根据getItemId()
所得到得 View 的 Id 来判断。
2.4 从 RecycledViewPool 获取 View:getRecycledView()
我们跳过中间的getViewForPositionAndType()
,即从三级缓存mViewCacheExtension
中寻找 View。因为三级缓存是我们自己定义的,什么时候缓存什么时候复用都是自己定义的,而且现实中用到的比较少。 ,
所以往下看,我们从mChangedScrap
、mAttachedScrap
和mCachedViews
中都拿不到 View,于是到四级缓存RecycledViewPool
中查找,调用方法getRecycledView()
。
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
这里的过程就比较简单了,我们之前已经知道了scrapHeap
就是存放 ViewHolder 的列表,其中 i 给你如果有 ViewHolder,就取出一个;如果没有,就返回null
。
3. (创建 View)绑定数据:
当然,如果还是没有 ViewHolder,即仍然holder==null
,那么就会调用我们熟悉的mAdapter.createViewHolder()
方法,即我们自定义的创建 ViewHolder 的方法。
之后就是尝试绑定数据,在tryBindViewHolderByDeadline()
中会调用mAdapter.bindViewHolder()
,这个方法中就调用了我们熟悉的onBindViewHolder()
。当然,这时候就进入绑定数据的功能了,所以工作者从Recycler转移到了Adapter。
总结
同理,这里的入口是滑动产生的复用情况。实际上很多其他时候都会有复用的情况,比如刷新后的初始布局等操作。
onTouchEvent()
-> scrollByInternal()
-> scrollStep()
-> (LayoutManager) scrollHorizontallyBy() / scrollVerticallyBy()
-> scrollBy()
-> fill()
-> layoutChunk() -> next()
-> (Recycler) getViewForPosition()
-> tryGetViewHolderForPositionByDeadline()
(该方法返回 ViewHolder,其内部流程如下)
getChangedScrapViewForPosition()
从mChangedScrap获取,没拿到就去getScrapOrHiddenOrCachedHolderForPosition()
和getScrapOrCachedViewForId()
从mAttachedScrap和mCachedViews获取,没拿到就去getViewForPositionAndType()
从自定义mViewCacheExtension中获取,没拿到就去getRecycledView()
从RecycledViewPool中获取,没拿到就创建createViewHolder()
创建一个新的。最后调用tryBindViewHolderByDeadline() -> bindViewHolder() -> onBindViewHolder()
绑定数据。
后记
这是真的写了我好久时间,花费的精力也好多。不过也有可能是我边玩边写的原因=。=
不过为了这篇我还特地去画流程图,去 PS 做图。所以还是得夸夸自己
真是麻雀啄牛屁股——雀食牛 x
也不知道什么时候才能真正做到周更=。=