RecyclerView缓存

·  阅读 311

缓存结构

Recycler缓存ViewHolder对象有4个等级,优先级从高到底依次为:

  • ArrayList mAttachedScrap
  • ArrayList mCachedViews
    • mAttachedScrap和mCachedViews的是否被命中是通过如下几个条件
      1. Item是否被移除
      2. Item的ViewType是否相同
      3. ItemId是否相同
  • ViewCacheExtension mViewCacheExtension
  • RecycledViewPool mRecyclerPool
缓存涉及对象作用重新创建视图View(onCreateViewHolder)重新绑定数据(onBindViewHolder)
一级缓存mAttachedScrap缓存屏幕中可见范围的ViewHolderfalsefalse
二级缓存mCachedViews缓存滑动时即将与RecyclerView分离的ViewHolder,按子View的position或id缓存,默认最多存放2个falsefalse
三级缓存mViewCacheExtension开发者自行实现的缓存--
四级缓存mRecyclerPoolViewHolder缓存池,本质上是一个SparseArray,其中key是ViewType(int类型),value存放的是 ArrayList< ViewHolder>,默认每个ArrayList中最多存放5个ViewHolderfalsetrue
  1. RecyclerView滑动时会触发onTouchEvent#onMove,回收及复用ViewHolder在这里就会开始。
  2. LayoutManager负责RecyclerView的布局,包含对ItemView的获取与复用。
  3. 最终会调用到tryGetViewHolderForPositionByDeadline()方法返回ViewHolder。

aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy81ODcxNjMtNjlhNmYwZTIzMzQwNjc1Ni5wbmc.png

  • 通过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; // 当前布局阶段
        }
    }
    复制代码
    1. 当在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;
            }
        }
    }
    复制代码
    1. InfoRecord中用一个int类型的标志位来标识Item经历过哪些布局阶段。若Item动画信息是在 post-layout 阶段被添加的,其标志位会追加FLAG_POST(该标记位用于判断做什么类型的动画)。最后将表项动画信息和对应的 ViewHolder 相互绑定并存储到 ArrayMap 结构中。
    2. addToPreLayout()和addToPostLayout()分别在pre-layout和post-layout都会分别调用其方法,将 ViewHolder 和 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 实例并启动动画。

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改