Android TV开发-Recycler的缓存了解到放弃

CSDN文章地址:https://blog.csdn.net/qw85525006/article/details/91127988

@TOC

欢迎大家入坑.
大家好,我是冰雪情缘,已经在 Android TV开发爬坑多年,也是一名TV开发开源爱好者.

Android TV 开源社区 https://gitee.com/kumei/Android_tv_libs
Android TV 文章专题 https://www.jianshu.com/c/3f0ab61a1322
Android TV QQ群1:522186932 QQ群2:468357191

1. 写在前面

为何要了解ReycclerView的缓存机制,
第一,能更合理的使用缓存,保证应用的流畅性,低耗能;
第二,优化的能更到位;
第三,基础更扎实,后续 提升技术能力的基石.
下面我们主要是从优化的角度去看缓存流程(参考源码 android-24).

2. Recycler的几个函数

RecyclerView相关的 Recycler 几个函数:

  • setItemViewCacheSize 设置 cacheView缓存大小
  • setViewCacheExtension 设置自定义缓存
  • setRecycledViewPool 设置缓存池
  • setRecyclerListener 回调

3. Recycler 获取缓存的流程

下图为 缓存的查找过程(画了好久...),直到找不到缓存,进行 创建 ViewHolder.


在这里插入图片描述

配合流程图,我们来看看具体的代码过程:

RecyclerView.Recycler

public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}

// 根据position 获取ViewHolder.view 
// ViewHolder(缩写 VH)
View getViewForPosition(int position, boolean dryRun) {
    // 1. mChangedScrap  getChangedScrapViewForPosition 
    //    1.1 根据 position 获取 VH 缓存
    //    1.2 根据 id 获取 VH 缓存
    holder = getChangedScrapViewForPosition(position);
    // 2. getScrapViewForPosition 
    //    2.1 mAttachedScrap 根据 position 获取 VH 缓存
    //    2.2 ChildHelper找到隐藏与没移除的View,通过getChildViewHolderInt获取 VH 缓存
    //    2.3 mCachedViews 根据 position 获取 VH 缓存
    holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
    // 3. getScrapViewForId 
    //    3.1 mAttachedScrap 根据 id 获取 VH 缓存
    //    3.2 mCachedViews 根据 id 获取 VH 缓存
    holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
    // 4. mViewCacheExtension 获取 VH 缓存
    final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
    // 5. RecycledViewPool 获取 VH 缓存
    holder = getRecycledViewPool().getRecycledView(type);
    // 6. 创建 ViewHolder
    holder = mAdapter.createViewHolder(RecyclerView.this, type);
    ... ...
    // 判断是否要重新绑定 ViewHolder
    if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        // 绑定 ViewHolder
        mAdapter.bindViewHolder(holder, offsetPosition);
    }
    ... ...
}

我们看到代码出出现了 mChangedScrap,mAttachedScrap,mCachedViews,mViewCacheExtension,RecycledViewPool
这些都是什么意思?我们先来看看一段代码

public final class Recycler {
    private ArrayList mChangedScrap = null; // 数据已经改变的 VH 列表
    final ArrayList mAttachedScrap = new ArrayList<>(); // 与RecyclerView分离的 VH 列表
    private int mViewCacheMax = DEFAULT_CACHE_SIZE; // DEFAULT_CACHE_SIZE=2        
    final ArrayList mCachedViews = new ArrayList();
    private ViewCacheExtension mViewCacheExtension;  // 开发者可自定义的缓存
    private RecycledViewPool mRecyclerPool; // ViewHolder缓存池
    ... ...
} 

列出一个表格说明对应的意思

缓存级别 createView bindView 变量 含义
一级缓存(Scrap View) mAttachedScrap和mChangedScrap 这是优先级最高的缓存,RecyclerView在获取ViewHolder时,优先会到这两个缓存来找。其中mAttachedScrap存储的是当前还在屏幕中的ViewHolder,mChangedScrap存储的是数据被更新的ViewHolder,比如说调用了Adapter的 notifyXXX 方法。匹配机制按照position和id进行匹配
二级缓存(Removeed View) mCachedViews 默认大小为2,缓存离开屏幕的viewHolder. 解决RecyclerView滑动抖动时的情况,还有用于保存Prefetch的ViewHoder.(这个版本android-24没有)
三级缓存(可选可配置) ViewCacheExtension 自定义缓存,通常用不到,getViewForPositionAndType 来实现自己的缓存 使用场景:位置固定 内容不变 数量有限
四级缓存(可配置) RecyclerViewPool 根据ViewType来缓存ViewHolder,每个ViewType的数组大小默认为5,可以动态的改变 缓存的ViewHolder需要重新绑定(bindView). 也可以 RecyclerView之间共享ViewHolder的缓存池Pool.

判断是否要重新绑定 ViewHolder,holder.isBound() || holder.needsUpdate() || holder.isInvalid(),下列看看全部相关意义。
ViewHolder的isInvalid、isRemoved、isBound、isTmpDetached、isScrap和isUpdated这几个方法。

方法名 对应的Flag 含义或者状态设置的时机
isInvalid FLAG_INVALID 表示当前ViewHolder是否已经失效。通常来说,在3种情况下会出现这种情况:1.调用了Adapter的notifyDataSetChanged方法; 2. 手动调用RecyclerView的invalidateItemDecorations方法; 3. 调用RecyclerView的setAdapter方法或者swapAdapter方法。
isRemoved FLAG_REMOVED 表示当前的ViewHolder是否被移除。通常来说,数据源被移除了部分数据,然后调用Adapter的notifyItemRemoved方法。
isBound FLAG_BOUND 表示当前ViewHolder是否已经调用了onBindViewHolder。
isTmpDetached FLAG_TMP_DETACHED 表示当前的ItemView是否从RecyclerView(即父View)detach掉。通常来说有两种情况下会出现这种情况:1.手动了RecyclerView的detachView相关方法;2. 在从mHideViews里面获取ViewHolder,会先detach掉这个ViewHolder关联的ItemView
isScrap 无Flag来表示该状态,用mScrapContainer是否为null来判断 表示是否在mAttachedScrap或者mChangedScrap数组里面,进而表示当前ViewHolder是否被废弃。
isUpdated FLAG_UPDATE 表示当前ViewHolder是否已经更新。通常来说,在3种情况下会出现情况:1.isInvalid方法存在的三种情况;2.调用了Adapter的onBindViewHolder方法;3. 调用了Adapter的notifyItemChanged方法

4. Recycler 存储缓存的流程

在这里插入图片描述

遥控器按下键往下走的时候(CacheView默认为2,类型(type=1)一致).

  • 退出屏幕的时候:


    在这里插入图片描述
  1. 当ViewHolder (position=0) 出屏幕的时候,放入 CacheView.
  2. ViewHolder(position=1) 同步骤1.
  3. ViewHolder(position=2),一级缓存mCacheView满了,ViewHolder(position=0) 从 mCacheView移除,放入 缓冲池(RecyclerPool, type=1),ViewHolder(position=2) 放入 mCacheView.


    在这里插入图片描述
  • 进屏幕时候的情况:
  1. 当ViewHolder(position=0-2)出屏幕的时候,ViewHolder(position=6~7)进入屏幕,6-7由于缓存找不到对应的,然后会 createViewHolder来创建ViewHolder,adapter.bindViewHolder绑定.
  2. 当ViewHolder(position=8)进入屏幕的时候, 在 缓存池中存在 type=1(就是出屏幕的 ViewHolder(position=0)),复用,不需要创建,但需要重新 bindViewHolder.

当 Item 滑离屏幕的时候,会被缓存起来,这里缓存指的是 mCacheView,mRecyclerViewPool
主要看 recycleViewHolderInternal 的源码分析(又到了贴代码的时间了,注意!!~!!!)

遥控器按上键往上走的时候(CacheView默认为2,类型(type=1)一致). 如果当前开始位置为postion=3

  1. ViewHolder(position=2),mCacheView存在,所以直接返回,不需要重新绑定.(参考前面的流程图)
  2. ViewHolder(position=1),同步骤1.
  3. ViewHolder(position=0),由于mCacheView找不到,mRecyclerViewPool 存在,使用,并进行bindViewHolder.

具体滑动 存储缓存的流程图如下:

在这里插入图片描述

题外话: 关于 fill,想了解的 小伙伴,可以去关注下 自定义 Layoutmanger,onLayoutChildren
进行自定义的布局之前:

  1. 调用detachAndScrapAttachedViews方法把屏幕中的Items都分离出来,内部调整好位置和数据后,
    detachAndScrapAttachedViews(recycler)这个方法就是将所有的view缓存在scrap里 (并放进mAttachedScrap中)。
    先把所有的View先从RecyclerView中detach掉,然后标记为"Scrap"状态,表示这些View处于可被重用状态(非显示中)。
    实际就是把View放到了Recycler中的一个集合中。
  2. 调用 Recycler的getViewForPosition(int position) 方法来获取,通过addView方法来添加.
  3. 获取到Item并重新添加了之后,需要对它进行测量,这时候可以调用measureChild或measureChildWithMargins方法,
  4. 根据需求来决定使用 layoutDecorated 还是 layoutDecoratedWithMargins 方法;

在自定义ViewGroup中,layout完就可以运行看效果了,但在LayoutManager还有一件非常重要的事情,就是回收了,我们在layout之后,还要把一些不再需要的Items回收,以保证滑动的流畅度;

当 Item 滑离屏幕的时候,会被缓存起来,这里缓存指的是 mCacheView,mRecyclerViewPool
主要看 recycleViewHolderInternal 的源码分析(又到了贴代码的时间了,注意!!~!!!)

Recycle
void recycleViewHolderInternal(ViewHolder holder) {
    ... ...
    if (forceRecycle || holder.isRecyclable()) {
        if (!holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved()) &&
                !holder.isChanged()) {
            // Retire oldest cached view
            // 如果没有调用 setItemViewCacheSize 设置,默认为 2 个.
            final int cachedViewSize = mCachedViews.size();
            if (cachedViewSize == mViewCacheMax && cachedViewSize > 0) {
                recycleCachedViewAt(0);
            }
            if (cachedViewSize < mViewCacheMax) {
                mCachedViews.add(holder);
                cached = true;
            }
        }
        // 如果cachedViewSize 超过 mViewCacheMax(默认2),就添加到 mRecyclerViewPool.
        if (!cached) {
            addViewHolderToRecycledViewPool(holder);
            recycled = true;
        }
    }
    ... ...
}

void addViewHolderToRecycledViewPool(ViewHolder holder) {
    ViewCompat.setAccessibilityDelegate(holder.itemView, null);
    dispatchViewRecycled(holder);
    // setRecyclerListener  mRecyclerListener.onViewRecycled(holder); 
    // Adapter mAdapter.onViewRecycled(holder);
    holder.mOwnerRecyclerView = null;
    // putRecycledView 获取 holder getItemViewType 的类型,用于缓存存储.
    getRecycledViewPool().putRecycledView(holder);
}

5. 优化

根据 Recycler 获取或者存储 缓存的流程,我们知道 RecyclerView 优化 最重要是 减少 createViewHolder, bindViewHolder耗时调用次数,下面我们将围绕着两个东西来讲解下一些简单的优化事宜.

在这里插入图片描述

1. onCreateViewHolder
在这里插入图片描述

2. onBindViewHolder
在这里插入图片描述

根据上面的两个内容再补充一些细节:

  • 合理使用缓存设置(setItemViewCacheSize,setViewCacheExtension,setRecycledViewPool)
  • 注意耗时操作(尤其是屏幕滚动的时候,尽量 停止加载的操作)
  • 减少布局结构、减少过渡绘制,可以提高item的 measure 与 draw 的效率。也尽量避免多次measure & layout 次数(比如TextView可以进行有效优化)
    TextView 可以使用 使用 StaticLayout 或者 DynamicLayout 的自定义 View 来代替它
  • 取消默认动画
    mRecyclerView.setItemAnimator(null); 也可以改善一点点.
  • 调整draw缓存
    mRecyclerView.setDrawingCacheEnabled(true); mRecyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
  • 慎用Alpha(不管是图片还是View,都需要注意下)
    也可以尝试重写 View 的 hasOverlappingRendering return false,提升一点点性能
  • 尽量使用稳定的高版本RecyclerView,比如新版本(25.1.0 及以上)有 Prefetch 功能
  • Item 高度是固定的话,RecyclerView.setHasFixedSize(true)
  • onViewRecycled 可以回收一些资源.
  • 设置更多预留空间(屏幕显示范围之外),重写 getExtraLayoutSpace
    启动应用后,如果一整屏 item的时候,向下滑动,RecyclerView找不到缓存,它将创建一个新的item,导致有点延时的感觉.
  • 根据不同的机型(CPU,内存,内存使用情况 等等) 特性 进行对应的 优化策略(空间与时间 不可能达到100%完美平衡)
    也可以了解下这个几个函数(onTrimMemory,onLowMemory)

6. 参考资料

Anatomy of RecyclerView: a Search for a ViewHolder (continued) [需要翻墙]
https://android.jlelse.eu/anatomy-of-recyclerview-part-1-a-search-for-a-viewholder-continued-d81c631a2b91
RecyclerView缓存原理,有图有真相
https://juejin.im/post/5b79a0b851882542b13d204b
RecyclerView 缓存机制详解
https://blog.csdn.net/zhangqilugrubby/article/details/53463875

7. 结束语

一直没有时间整理,花了3周一点点的整理,终于完成了.
由于本人技术水平有限,有问题的地方还望一起探讨,学习,进步,谢谢.
不要忘记点赞,关注。

Android TV 开源社区 https://gitee.com/kumei/Android_tv_libs
Android TV 文章专题 https://www.jianshu.com/c/3f0ab61a1322
Android TV QQ群1:522186932 QQ群2:468357191

你可能感兴趣的:(Android TV开发-Recycler的缓存了解到放弃)