前言
本文主要通过如下几个方面讲解RecyclerView缓存机制:
- ViewHolder
- 缓存架构
- Recycler.recycleViewHolderInternal(ViewHolder holder)
- Recycler.tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs)
1. ViewHolder
1.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缓存中
1.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动画
2. 缓存架构
2.1 四级缓存
- ArrayList<ViewHolder> mAttachedScrap & ArrayListmChangedScrap
- ArrayList<ViewHolder> mCachedViews
- ViewCacheExtension mViewCacheExtension
- RecycledViewPool mRecyclerPool
2.2 scrap缓存
scrap缓存由mAttachedScrap和mChangedScrap两个缓存组成,在RecyclerView调用dispatchLayout时会使用该缓存,保存RecyclerView上的子View。
两部分组成
- mAttachedScrap
- mChangedScrap
缓存特性
- 对应的数据结构是ArrayList<ViewHolder>
- 缓存大小没有限制,大小等于RecyclerView子View的个数
- 该缓存中的ViewHolder无需重新绑定,只要ViewHolder的position和数据源中的position对应上了
- 调用notifyItemRemoved、notifyItemMoved、notifyItemInserted方法,ViewHolder放入mAttachedScrap中
- 调用notifyItemChanged(int position, Object payload),如果payload!=null ViewHolder放入mAttachedScrap中,否则ViewHolder放入mChangedScrap中
- 调用notifyDataSetChanged()时,如果Adapter.hasStableIds返回true,ViewHolder放入mAttachedScrap中,否则会将ViewHolder回收到非scrap缓存中
- LinearLayoutManager.layoutForPredictiveAnimations()阶段,mAttachedScrap数组剩下的ViewHolder是被挤出屏幕的
2.3 mCachedViews缓存
缓存特性
- 对应的数据结构是ArrayList<ViewHolder>
- 缓存大小有限制,默认缓存大小为2,可以修改默认缓存大小。如果使用GridLayoutManager建议设置为列的个数
- 该缓存中的ViewHolder无需重新绑定,只要ViewHolder的position和数据源中的position和itemType对应上了
- 该缓存的特性是FIFO
- ViewHolder mFlag如果有FLAG_INVALID、FLAG_REMOVED、FLAG_UPDATE、FLAG_ADAPTER_POSITION_UNKNOWN之一,不会放入该缓存
- 当RecyclerView滑动时会将ViewHolder放入该缓存或者从该缓存获取ViewHolder
2.4 ViewCacheExtension
该接口只提供了get方法,没提供put方法。
2.5 RecyclerViewPool
缓存特性
- 对应的数据结构是SparseArray<ScrapData>,根据itemType将缓存分组,组的数据结构是ScrapData
- ScrapData对应的数据结构是ArrayList<ViewHolder>,每个itemType对应的ScrapData的缓存大小默认值是5,可以修改缓存大小
- 该缓存中的ViewHolder需要重新绑定数据
- 可以提供给多个RecyclerView共享
3. Recycler.recycleViewHolderInternal(ViewHolder holder)
3.1 调用时机
- 调用notifyItemRemoved(),ViewHolder动画结束后
- 被挤出屏幕的ViewHolder在动画结束后
- 滑出屏幕的ViewHolder需要回收掉
- 调用notifyDataSetChanged(),而且Adapter.hasStableIds()返回false
- 缓存中获取到的ViewHolder校验失败,需要回收
- 调用setAdapter()时
3.2 回收逻辑
3.2.1 回收流程图
3.2.2 源码分析
- 如果当前mCachedViews容量>=缓存最大数量,将存放最久的ViewHolder回收到RecyclerViewPool中,直到mCachedViews size=mViewCacheMax-1
- 将ViewHolder缓存到mCachedViews最后一个槽位上
- 如果缓存到mCachedViews失败,则缓存到RecyclerViewPool中
3.2.2.1 Recycler.recycleViewHolderInternal()
3.2.2.2 Recycler.recycleCachedViewAt(int cachedViewIndex)
3.2.2.3 RecyclerViewPool.putRecycledView(ViewHolder scrap)
4. Recycler.tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs)
4.1 调用时机(以LinearLayoutManager为例)
dispatchLayoutStep1()、dispatchLayoutStep2()、滑动RecyclerView时会发生复用
- LinearLayoutManager.onLayoutChildren() -> LinearLayoutManager.fill() -> LinearLayoutManager.layoutChunk() -> LinearLayoutManager.LayoutState.next()-> RecyclerView.Recycler.getViewForPosition()
- LinearLayoutManager.layoutForPredictiveAnimations()-> LinearLayoutManager.fill() -> LinearLayoutManager.layoutChunk() -> LinearLayoutManager.LayoutState.next()-> RecyclerView.Recycler.getViewForPosition()
- LinearLayoutManager.scrollBy()-> LinearLayoutManager.fill() -> LinearLayoutManager.layoutChunk() -> LinearLayoutManager.LayoutState.next()-> RecyclerView.Recycler.getViewForPosition()
4.2 获取ViewHolder流程
4.2.1 Recycler.getChangedScrapViewForPosition(int position)
预布局过程中从mChangedScrap缓存中获取ViewHolder。获取逻辑如下:
- 线性遍历 mChangedScrap,position == ViewHolder.mPreLayoutPosition,返回该ViewHolder,否则走逻辑2
- Adapter.hasStableIds()返回false,返回null,否则走逻辑3
- 线性遍历 mChangedScrap,mAdapter.getItemId(offsetPosition) == holder.getItemId,返回该ViewHolder,否则走逻辑4
- 上述都没有获取到,返回null。
4.2.2 Recycler.getScrapOrHiddenOrCachedHolderForPosition(int position)
4.2.2.1 该方法从三个地方获取缓存,获取后还需要验证是否合法,如果验证失败会调用Recycler.recycleViewHolderInternal()
- mAttachedScrap
- hidden list中,ChildHelper通过位图算法,逻辑隐藏的ViewHolder
- mCachedViews
4.2.2.2 从mAttachedScrap中获取,必须满足以下所有条件:
- holder.getLayoutPosition() == position,即预布局 viewHolder.mPreLayoutPosition == position,布局中 viewHolder.mPosition == position
- !holder.isInvalid()
- 非预布局情况下,不可返回removed的ViewHolder
4.2.2.3 从hidden list中获取ViewHolder
- 调用调用ChildHelper.findHiddenNonRemovedView(position)获取View
- 调用ChildHelper.unhide(view),不隐藏View
- 调用mChildHelper.detachViewFromParent(layoutIndex),轻量删除View,从RecyclerView children数组中删除,但是不会重新绘制RecyclerView
- 放入scrap缓存中
4.2.2.4 从mCachedViews中获取ViewHolder
- !holder.isInvalid()
- holder.getLayoutPosition() == position
- 如果满足会将ViewHolder从mCachedViews中移除
4.2.3 Recycler.getScrapOrCachedViewForId(long id, int type, boolean dryRun)
- 首先从attachedScrap中查找,如果id和type都匹配上了,返回该ViewHolder,并且在非预布局情况下,将removed的设置成update
- 如果id匹配上了 type没匹配上,将该ViewHolder回收掉
- 其次从mCachedViews中查找,如果id和type都匹配上了,返回并移除掉该ViewHolder
- 如果id匹配上了 type没匹配上,将该ViewHolder回收掉
4.2.4 getRecycledViewPool().getRecycledView(int type)
根据viewType从SparseArray中获取相应的ViewHolder,该缓存中获取的ViewHolder需要重新绑定
4.2.5 Adapter.createViewHolder(RecyclerView recyclerView, int type)
4.2.6 Recycler.tryBindViewHolderByDeadline(RecyclerView recyclerView, int type)
- 调用Adapter.bindViewHolder()方法
- 将ViewHolder flag设置为FLAG_BOUND,清除FLAG_UPDATE、FLAG_INVALID、FLAG_ADAPTER_POSITION_UNKNOWN
- 重写onBindViewHolder(VH holder, int position, List<Object> payloads),通过payloads可以实现局部刷新