缓存结构
Recycler缓存ViewHolder对象有4个等级,优先级从高到底依次为:
- ArrayList mAttachedScrap
- ArrayList mCachedViews
- mAttachedScrap和mCachedViews的是否被命中是通过如下几个条件
- Item是否被移除
- Item的ViewType是否相同
- ItemId是否相同
- mAttachedScrap和mCachedViews的是否被命中是通过如下几个条件
- ViewCacheExtension mViewCacheExtension
- RecycledViewPool mRecyclerPool
缓存 | 涉及对象 | 作用 | 重新创建视图View(onCreateViewHolder) | 重新绑定数据(onBindViewHolder) |
---|---|---|---|---|
一级缓存 | mAttachedScrap | 缓存屏幕中可见范围的ViewHolder | false | false |
二级缓存 | mCachedViews | 缓存滑动时即将与RecyclerView分离的ViewHolder,按子View的position或id缓存,默认最多存放2个 | false | false |
三级缓存 | mViewCacheExtension | 开发者自行实现的缓存 | - | - |
四级缓存 | mRecyclerPool | ViewHolder缓存池,本质上是一个SparseArray,其中key是ViewType(int类型),value存放的是 ArrayList< ViewHolder>,默认每个ArrayList中最多存放5个ViewHolder | false | true |
- RecyclerView滑动时会触发onTouchEvent#onMove,回收及复用ViewHolder在这里就会开始。
- LayoutManager负责RecyclerView的布局,包含对ItemView的获取与复用。
- 最终会调用到tryGetViewHolderForPositionByDeadline()方法返回ViewHolder。
- 通过mAttachedScrap、mCachedViews及mViewCacheExtension获取的ViewHolder不需要重新创建布局及绑定数据。
- 通过缓存池mRecyclerPool获取的ViewHolder不需要重新创建布局,但是需要重新绑定数据。
- 如果上述缓存中都没有获取到目标ViewHolder,那么就会回调Adapter#onCreateViewHolder创建布局,以及回调Adapter#onBindViewHolder来绑定数据。
ViewCacheExtension
- ViewHolder位置固定、内容固定、数量有限时使用
//1.viewType类型为TYPE_SPECIAL时,设置四级缓存池RecyclerPool不存储对应类型的数据 因为需要开发者自行缓存
recyclerView.getRecycledViewPool().setMaxRecycledViews(DemoAdapter.TYPE_SPECIAL, 0);
//2.设置ViewCacheExtension缓存
recyclerView.setViewCacheExtension(new MyViewCacheExtension());
//3.实现自定义缓存ViewCacheExtension
class MyViewCacheExtension extends RecyclerView.ViewCacheExtension {
@Nullable
@Override
public View getViewForPositionAndType(@NonNull RecyclerView.Recycler recycler, int position, int viewType) {
//如果viewType为TYPE_SPECIAL,使用自己缓存的View去构建ViewHolder
// 否则返回null,会使用系统RecyclerPool缓存或者从新通过onCreateViewHolder构建View及ViewHolder
return viewType == DemoAdapter.TYPE_SPECIAL ? adapter.caches.get(position) : null;
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (holder instanceof SpecialHolder) {
SpecialHolder sHolder = (SpecialHolder) holder;
sHolder.tv_ad.setText(mDatas.get(position));
//4.这里是重点,根据position将View放到自定义缓存中
caches.put(position, sHolder.itemView);
} else if (holder instanceof CommonHolder) {
....
}
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return TYPE_SPECIAL;//5.第一个位置View和数据固定
} else {
return TYPE_COMMON;
}
}
复制代码
- 在设置了ViewCacheExtension 缓存后,ViewHolder首次创建完毕之后会通过getViewForPositionAndType回调之中根据position和type获得到了缓存的ViewCache
Item如何回收
limit隐形线
RecyclerView会根据滚动的位移值去计算具体填充的item,同时要通过Limit隐形线进行回收Item
- Iimit隐形线的初始值是RecyclerView当前尾部Item至列表底部的距离,也就是在不填充新Item的情况下,最大滑动距离
- 每次新填充的Item消耗的像素值都会叠加到Limit隐形线上,也就是Limit隐形线会跟随Item的填充二不断下移
- Limit隐形线之上的Item都会被回收
- 每填充一个新的Item都会检测已加载的Item之中有没有可被回收的
Item动画
- 除了在Item动画执行完毕,会把事件传递给RecyclerView内部的监听器,监听器触发回收Item
Item移除
如果Item被移出屏幕,则会被回收至mCachedViews之中,如果恰巧此Item被删除,则从mCachedViews删除,添加至mRecyclerPool之中
pre-layout & post-layout
- 为了实现Item动画,会在动画开始和结束各通过两次布局,分别是 pre-layout 和 post-layout,获得对应快照实现动画。
RecyclerView
为了实现Item动画,进行了 2 次布局,第一次预布局,第二次正真的布局,在源码上表现为LayoutManager.onLayoutChildren()
被调用 2 次mState.mInPreLayout
的值标记了预布局的生命周期。预布局的过程始于RecyclerView.dispatchLayoutStep1()
,终于RecyclerView.dispatchLayoutStep2()
。两次调用LayoutManager.onLayoutChildren()
会因为这个标记位的不同而执行不同的逻辑分支。- 实现会通过dispatchLayoutStep1()方法之中,调用到LayoutManager.onLayoutChildren()方法,该方法用于布局 Adapter 中所有的Item。若支持Item动画,则 onLayoutChildren() 会被调用 2 次,第一次称为 pre-layout,它是真正布局Item之前的一次预布局。
LayoutManager.onLayoutChildren()
- 为了让两次布局不互相影响,所以要要在每次布局前清除上一次布局的内容,但是两次布局基本上是变化不大的,所以Recyclerview采用了时间换空间,在清除之前把Item缓存至Scrap中,在具体填充时就可以通过缓存获取对应的ViewHolder。
- 在预布局阶段,循环填充Item时,若遇到被删除的Item,则会忽略此Item占用的空间,因为此Item最终不会被加载至屏幕之中。
- detach view和remove view差不多,它们都会将子控件从父控件的孩子列表中删除,唯一的区别是detach更轻量,不会触发重绘。而且detach是短暂的,并同时缓存入 mAttachedScrap列表中。在紧接着的填充表项阶段,就立马从mAttachedScrap中取出刚被 detach 的表项并重新 attach 它们
动画处理
-
RecyclerView用一个Int值去标记不同的状态
public class RecyclerView { public static class State { static final int STEP_START = 1;//布局的开始阶段 static final int STEP_LAYOUT = 1 << 1;//布局的布局阶段 static final int STEP_ANIMATIONS = 1 << 2; // 布局的动画阶段 int mLayoutStep = STEP_START; // 当前布局阶段 } } 复制代码
- 当在dispatchLayoutStep2()结束就会标记状态为STEP_ANIMATIONS,即进入布局动画阶段,dispatchLayoutStep3()即为动画处理
-
通过ItemAnimator.recordPostLayoutInformation()逐个构建Item动画信息ItemHolderInfo,ItemHolderInfo格式如下
// Item信息实体类 public static class ItemHolderInfo { // 上下左右相对于列表的距离 public int left; public int top; public int right; public int bottom; public ItemHolderInfo setFrom(RecyclerView.ViewHolder holder) { return setFrom(holder, 0); } // 记录Item的位置 public ItemHolderInfo setFrom(RecyclerView.ViewHolder holder,int flags) { final View view = holder.itemView; this.left = view.getLeft(); this.top = view.getTop(); this.right = view.getRight(); this.bottom = view.getBottom(); return this; } } 复制代码
-
ItemHolderInfo包含了Item的相对位置信息,然后会保存至ViewInfoStore之中,ViewInfoStore用于专门保存Item动画信息
class ViewInfoStore { //使用ArrayMap存储ViewHolder 与其对应动画信息 final ArrayMap<RecyclerView.ViewHolder, InfoRecord> mLayoutHolderMap = new ArrayMap<>(); //存储Post—Layout阶段的Item与其动画信息 void addToPostLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) { InfoRecord record = mLayoutHolderMap.get(holder); if (record == null) { // 从池中获取 InfoRecord 实例 record = InfoRecord.obtain(); // 将 ViewHolder 和 InfoRecord 绑定 mLayoutHolderMap.put(holder, record); } record.postInfo = info; // 将后布局表项动画信息存储在 postInfo 字段中 record.flags |= FLAG_POST; // 追加 FLAG_POST 到标志位 } // 存储pre-layout Item与其动画信息 void addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) { InfoRecord record = mLayoutHolderMap.get(holder); if (record == null) { record = InfoRecord.obtain(); mLayoutHolderMap.put(holder, record); } record.preInfo = info; // 将后布局表项动画信息存储在 preInfo 字段中 record.flags |= FLAG_PRE; // 追加 FLAG_PRE 到标志位 } static class InfoRecord { int flags; // 标记位 static final int FLAG_PRE = 1 << 2; // pre-layout 标记 static final int FLAG_POST = 1 << 3; // post-layout 标记 static final int FLAG_APPEAR = 1 << 1; // Item出现标志 RecyclerView.ItemAnimator.ItemHolderInfo preInfo;// pre-layout Item位置信息 RecyclerView.ItemAnimator.ItemHolderInfo postInfo;// post-layout Item位置信息 // 池:为避免内存抖动 static Pools.Pool<InfoRecord> sPool = new Pools.SimplePool<>(20); // 从池中获取 InfoRecord 实例 static InfoRecord obtain() { InfoRecord record = sPool.acquire(); return record == null ? new InfoRecord() : record; } } } 复制代码
- InfoRecord中用一个
int
类型的标志位来标识Item经历过哪些布局阶段。若Item动画信息是在 post-layout 阶段被添加的,其标志位会追加FLAG_POST
(该标记位用于判断做什么类型的动画)。最后将表项动画信息和对应的 ViewHolder 相互绑定并存储到 ArrayMap 结构中。 - addToPreLayout()和addToPostLayout()分别在pre-layout和post-layout都会分别调用其方法,将 ViewHolder 和 InfoRecord 进行绑定
- InfoRecord中用一个
-
RecyclerView经历了预布局和后布局和动画布局阶段,ViewInfoStore之中就保存了每一个参与动画的Item,内部包含了预布局位置信息 + 后布局位置信息 + 经历过的布局阶段,最终调用process()执行动画。
-
动画执行阶段会遍历ViewInfoStore中所有保存的Item,以标志位依据,执行具体的动画,动画回调类型如下:
interface ProcessCallback { //消失动画,处于布局前操作,不会添加到布局之中 void processDisappeared(RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ItemAnimator.ItemHolderInfo preInfo, @Nullable RecyclerView.ItemAnimator.ItemHolderInfo postInfo); //出现动画,出现在布局之中,但是并不重新适配 void processAppeared(RecyclerView.ViewHolder viewHolder, @Nullable RecyclerView.ItemAnimator.ItemHolderInfo preInfo, RecyclerView.ItemAnimator.ItemHolderInfo postInfo); //保持动画,两个动画都保持不动 void processPersistent(RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ItemAnimator.ItemHolderInfo preInfo, @NonNull RecyclerView.ItemAnimator.ItemHolderInfo postInfo); //出现后直接消失,对动画无效,普通回调 void unused(RecyclerView.ViewHolder holder); } 复制代码
-
收到动画指令和数据后,又将他们封装为MoveInfo,不同类型的动画被存储在不同的MoveInfo列表中。然后将执行动画的逻辑抛到 Choreographer 的动画队列中,当下一个垂直同步信号到来时,Choreographer 从动画队列中取出并执行表项动画,执行动画即遍历所有的
MoveInfo
列表,为每一个MoveInfo
构建 ViewPropertyAnimator 实例并启动动画。