美文网首页Android TV
Android TV开发-Recycler的缓存了解到放弃

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

作者: 冰雪情缘long | 来源:发表于2019-07-07 23:56 被阅读1次

    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<ViewHolder> mChangedScrap = null; // 数据已经改变的 VH 列表
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>(); // 与RecyclerView分离的 VH 列表
        private int mViewCacheMax = DEFAULT_CACHE_SIZE; // DEFAULT_CACHE_SIZE=2        
        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
        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的缓存了解到放弃

        本文链接:https://www.haomeiwen.com/subject/otywhctx.html