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)一致).
-
退出屏幕的时候:
- 当ViewHolder (position=0) 出屏幕的时候,放入 CacheView.
- ViewHolder(position=1) 同步骤1.
-
ViewHolder(position=2),一级缓存mCacheView满了,ViewHolder(position=0) 从 mCacheView移除,放入 缓冲池(RecyclerPool, type=1),ViewHolder(position=2) 放入 mCacheView.
- 进屏幕时候的情况:
- 当ViewHolder(position=0-2)出屏幕的时候,ViewHolder(position=6~7)进入屏幕,6-7由于缓存找不到对应的,然后会 createViewHolder来创建ViewHolder,adapter.bindViewHolder绑定.
- 当ViewHolder(position=8)进入屏幕的时候, 在 缓存池中存在 type=1(就是出屏幕的 ViewHolder(position=0)),复用,不需要创建,但需要重新 bindViewHolder.
当 Item 滑离屏幕的时候,会被缓存起来,这里缓存指的是 mCacheView,mRecyclerViewPool
主要看 recycleViewHolderInternal 的源码分析(又到了贴代码的时间了,注意!!~!!!)
遥控器按上键往上走的时候(CacheView默认为2,类型(type=1)一致). 如果当前开始位置为postion=3
- ViewHolder(position=2),mCacheView存在,所以直接返回,不需要重新绑定.(参考前面的流程图)
- ViewHolder(position=1),同步骤1.
- ViewHolder(position=0),由于mCacheView找不到,mRecyclerViewPool 存在,使用,并进行bindViewHolder.
具体滑动 存储缓存的流程图如下:
题外话: 关于 fill,想了解的 小伙伴,可以去关注下 自定义 Layoutmanger,onLayoutChildren
进行自定义的布局之前:
- 调用detachAndScrapAttachedViews方法把屏幕中的Items都分离出来,内部调整好位置和数据后,
detachAndScrapAttachedViews(recycler)这个方法就是将所有的view缓存在scrap里 (并放进mAttachedScrap中)。
先把所有的View先从RecyclerView中detach掉,然后标记为"Scrap"状态,表示这些View处于可被重用状态(非显示中)。
实际就是把View放到了Recycler中的一个集合中。- 调用 Recycler的getViewForPosition(int position) 方法来获取,通过addView方法来添加.
- 获取到Item并重新添加了之后,需要对它进行测量,这时候可以调用measureChild或measureChildWithMargins方法,
- 根据需求来决定使用 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