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 文章专题 //www.greatytc.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 文章专题 //www.greatytc.com/c/3f0ab61a1322
Android TV QQ群1:522186932 QQ群2:468357191

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容

  • 序言 RecyclerView有三大典型的功能,一个是Recycler的缓存机制,一个LayoutManager的...
    HusterYP阅读 4,275评论 1 10
  • 前言 RecyclerView这个控件几乎所有的Android开发者都使用过(甚至不用加几乎),它是真的很好用,完...
    肖邦kaka阅读 33,865评论 22 165
  • Recycler类是RecyclerView内部final类,它管理scrapped(废弃)或detached(独...
    gczxbb阅读 3,661评论 0 4
  • 这篇文章分三个部分,简单跟大家讲一下 RecyclerView 的常用方法与奇葩用法;工作原理与ListView比...
    LucasAdam阅读 4,379评论 0 27
  • 已经很久没有用一天时间看完一本书了,自从有了娃,便为自己的懒癌症找到了妥妥的理由。那究竟是什么原因让我可以快速读...
    锘vs夏奈阅读 189评论 0 0