RecyclerView缓存以及优化—了解

欢迎大家入坑.
大家好,我是冰雪情缘,已经在 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

写在前面

不想看原理的小伙伴,可以直接去看后面优化相关的

为何要了解ReycclerView的缓存机制,
第一,能更合理的使用缓存,保证应用的流畅性,低耗能;
第二,优化的能更到位;
第三,基础更扎实,后续 提升技术能力的基石.

在这里插入图片描述 在这里插入图片描述

当你满心欢喜 写了一个类似上图的 几个 界面,但是手势或遥控器 操作的时候,不是卡顿,就是内存,CPU爆炸,是不是一脸蒙蔽,然后又无从下手,有那么几个函数,又不知道如何使用,性能优化也更无从下手,那么我们进入正题吧,先去了解缓存,再来根据缓存的知识点,进行优化的知识了解


理解缓存

简单了解RecyclerView的流程

我们知道 RecyclerView 的优化 最重要是 减少 onCreateViewHolder(缩减createVH), onBindViewHolder(缩减bindVH)耗时(时间)调用次数,下面我们将围绕着这两个东西来讲解一些的 优化 事宜与相关的 缓存 知识.

先看看 RecyclerView 整个流程的简单介绍的示意图:
在这里插入图片描述
RecyclerView 包含了很多东西,比如 ItemDecoration,ItemAnimator,LayoutManger,Recycler,ViewHolder,Adapter… …
这里我们主要讲解的是 Recycler 这个东西,它是什么?
Recycler 主要是负责 ViewHolder 的创建复用(缓存的管理)

可以看看一张ReyclerVie的类图,大概了解下他们之间的关系:
在这里插入图片描述
先简单的看看几个相关的函数:

RecyclerView相关函数 Recycler 相关函数 含义
setItemViewCacheSize setViewCacheSize 设置 cacheView缓存大小,默认2个
setRecycledViewPool 设置缓存池(RecycledViewPool),每种类型默认5个
setViewCacheExtension 设置自定义缓存
detachAndScrapAttachedViews
setRecyclerListener 设置缓存回调
getViewForPosition 获取ViewHolder,从缓存中获取,没有则创建
bindViewToPosition 绑定 ViewHolder,会调用 Adapter.bindViewHolder -> onBindViewHolder

有没有思考过,setItemViewCacheSize 什么时候设置,设置多少个,过程是如何的?
包括 setRecycledViewPool 也是,对于 createView, bindView 他们又有什么影响?共享 RecycledViewPool 改如何去弄?
如何加快我们的界面加载速度以及复用?
setViewCacheExtension 什么时候使用?
notify…刷新界面为何还是要重新创建或者绑定,到底这么回事?


获取ViewHolder的流程

先简单的介绍下获取ViewHolder的流程
onLayoutChildre 会调用 detachAndScrapAttachedViews 分离 ViewHolder 存储到相应的缓存(Recycler)中去

这里拿出了 LinearLayoutManger 布局的流程(简单的过程):
fill ->

// layoutChunk 代码
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
	View view = layoutState.next(recycler);
}

// LayoutState next 代码
View next(RecyclerView.Recycler recycler) {
    // getViewForPosition 这里就是获取 ViewHolder 的过程
    // Recycler 主要是负责 ViewHolder 的 创建 与 复用(缓存的管理)
	final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

那我们先来看看getViewForPosition获取ViewHolder的流程吧:
在这里插入图片描述
getViewForPosition 这个函数就是从 Recycler 取出 ViewHolder 交给 Adapter.
getViewForPosition 做了哪些事情?按照上面流程我们大概的了解到,就是将 Recycler 缓存中对应的 ViewHolder取出来,没有则重新调用 createVH 创建ViewHolder.

Recylcer 缓存包含了(ChangedScrapAttachedScrapCachedViewsViewCacheExtensionRecyclerPool),后面会详解去了解缓存相关的知识,我们先来看看代码的过程

// 按照流程图一步步看下去,我们一共分成了 7 步
ViewHolder holder = null;
// 第 1 步: 根据 position 获取 changedScrap(被更新的ViewHolder)
if (mState.isPreLayout()) {
	holder = getChangedScrapViewForPosition(position);
	fromScrapOrHiddenOrCache = holder != null;
}

// 第 2 步:根据 position 获取 AttachedScrap(还在屏幕中的ViewHolder),CachedViews(缓存离开屏幕的viewHolder)
if (holder == null) {
	holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}

// 第 3 步:根据 ItemId 获取 AttachedScrap,CachedViews
if (holder == null) {
	final int type = mAdapter.getItemViewType(offsetPosition);
	// 判断是否开启了 StableIds,如果要开启,需要调用 Adapter.setHasStableIds(true)
	if (mAdapter.hasStableIds()) {
		holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);
	}
}

// 第 4 步:根据 position, type 从 自定义缓存 获取 ViewHolder
if (holder == null && mViewCacheExtension != null) {
	final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
	if (view != null) {
    	holder = getChildViewHolder(view);
    }
}

// 第 5 步:根据 type 从 RecycledViewPool 获取对应的 ViewHolder
if (holder == null) {
	holder = getRecycledViewPool().getRecycledView(type);
}

// 第 6 步:创建 ViewHolder
if (holder == null) {
	holder = mAdapter.createViewHolder(RecyclerView.this, type);
}

// 第 7 步:判断是否需要 bindViewHolder
if (mState.isPreLayout() && holder.isBound()) {
	// do not update unless we absolutely have to.
	holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
	// 判断 是否bind过 或 是否已更新 或 是否失效
	// tryBindViewHolderByDeadline
	mAdapter.bindViewHolder(holder, offsetPosition);
}

上面已经大概的讲解了 getViewForPosition 从 Recycler 获取 ViewHolder 的过程,

这里我们大概了解下 Recycler 的几个缓存变量:

public final class Recycler {
	private ArrayList<ViewHolder> mChangedScrap = null; 
	final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();      
	final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>(); 
	private ViewCacheExtension mViewCacheExtension;
	private RecycledViewPool mRecyclerPool;
} 

这几个缓存变量有什么区别?
这是优先级最高的缓存,RecyclerView在获取ViewHolder时,优先会到这 mAttachedScrap 与 mChangedScrap 两个缓存来找。其次才是 mCachedViews,最后才是RecyclerViewPool。

列出一个表格说明对应的意思(需要注意create,bind,优化相关):

缓存级别 createVH bindVH 变量 含义
一级缓存(Scrap View) mAttachedScrap mAttachedScrap存储的是当前还在屏幕中的ViewHolder。匹配机制按照position和id进行匹配
一级缓存(Scrap View) mChangedScrap mChangedScrap存储的是数据被更新的ViewHolder,比如说调用了Adapter的 notifyXXX 方法
二级缓存(Cache View) mCachedViews 默认大小为2,缓存离开屏幕的viewHolder. 解决两点: 1. 频繁进入/离开屏幕的ViewHolder导致的内存抖动的问题;2.还有用于保存Prefetch的ViewHoder.
三级缓存(可选可配置) ViewCacheExtension 自定义缓存,通常用不到,getViewForPositionAndType 来实现自己的缓存 使用场景:位置固定 内容不变 数量有限
四级缓存(缓存池) RecyclerViewPool 根据ViewType来缓存ViewHolder,每个ViewType的数组大小默认为5,可以动态的改变 缓存的ViewHolder需要重新绑定(bindView). 也可以 RecyclerView之间共享ViewHolder的缓存池Pool.

其实再 bindView之前,有一个判断 !holder.isBound() || holder.needsUpdate() || holder.isInvalid(),这几个是什么意思呢?

判断是否要重新绑定 ViewHolder,holder.isBound() || holder.needsUpdate() || holder.isInvalid(),下列看看全部相关意义。

ViewHolder的 isInvalidisRemovedisBoundisTmpDetachedisScrapisUpdated 这几个方法:

方法名 对应的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方法

回收相关API

刚才在 onLayoutChildre 的 detachAndScrapAttachedViews 这个函数很关键,分离了 屏幕上 ViewHolder 存储到对应的缓存上。这里再普及下其它相关的函数:

方法名 含义
detachAndScrapAttachedViews(recycler) detach轻量回收所有View
detachAndScrapView(view, recycler) detach轻量回收指定View
detachView(view); 超级轻量回收一个View,马上就要添加回来
attachView(view); 将上个方法detach的View attach回来
recycler.recycleView(viewCache.valueAt(i)); detachView 后 没有attachView的话 就要真的回收掉他们

recycle真的回收一个View ,该View再次回来需要执行onBindViewHolder方法
removeAndRecycleView(View child, Recycler recycler)
removeAndRecycleAllViews(Recycler recycler);

detach 和recycle的时机
一个View只是暂时被清除掉,稍后立刻就要用到,使用detach。它会被缓存进scrapCache的区域。
一个View 不再显示在屏幕上,需要被清除掉,并且下次再显示它的时机目前未知 ,使用remove。它会被以viewType分组,缓存进RecyclerViewPool里。

了解detach/attach View
addView和removeView方法,操作容器内的子视图数组,触发视图重绘制,触发子视图attach和detached窗体回调。
addViewInLayout和removeViewInLayou方法,与上面一样,只是不会重绘视图
attachViewToParent和detachViewFromParent方法,只会操作容器内的子视图数组

注意: 一个View只被detach,没有被recycle的话,不会放进RecyclerViewPool里,会一直存在recycler的scrap 中。网上有人的Demo就是如此,因此View也没有被复用,有多少ItemCount,就会new出多少个ViewHolder。

题外话:想了解的 自定义 Layoutmanger,可以去了解下 onLayoutChildren
进行自定义的布局之前:

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

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


刷新界面-缓存的处理

notifyItemChanged对缓存的影响

同样只更新pos=1的数据,使用 notifyItemChanged(position)notifyDataSetChanged() 有什么区别?

notifyItemChanged(position)
在这里插入图片描述
notifyDataSetChanged()
在这里插入图片描述

名称 区别
notifyItemChanged(position),notifyItemChanged(int position, Object payload) 等 被更新的的item放入了ChangeScrp,不需要createVH,但是需要bindVH
notifyDataSetChanged() 0~4放入了RecyclerPool,不需要createVH,但是全部需要 bindVH

滚动-缓存的处理

往上滑动的缓存过程
在这里插入图片描述


优化了解

概述

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

onCreateViewHolder
在这里插入图片描述
onBindViewHolder
在这里插入图片描述
想详细了解绘制过度,耗时查找等优化事宜,参考这篇博客-性能优化学习笔记.
一些函数相关的Demo 可以看看这个

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

  • 合理使用缓存设置(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,导致有点延时的感觉.
  • RecyclerView没有ItemClick方法,根据前面缓存的了解,建议 onCreateViewHolder 添加一次,避免多次调用 onBindViewHolder。
  • diffutil 可以了解下,是一个不错的工具,可以 判断两个数据集的差距
  • swapadapter 也可以了解下. 以前 setAdapter 是要清空缓存的,可以很好的发挥缓存的性能. 应用场景是两个数据源有很大的相似部分的情况下。
  • linearlayoutmanager的onSaveInstanceState和onRestoreInstanceState可以了解下.
  • 根据不同的机型(CPU,内存,内存使用情况 等等) 特性 进行对应的 优化策略(空间与时间 不可能达到100%完美平衡)
    也可以了解下这个几个函数(onTrimMemory,onLowMemory)

onTrimMemory使用资料

onTrimMemory(int level) :
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN: // 你的用户界面不再可见,此时应该释放仅仅使用在UI上的大资源
ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE: // 系统处于低内存状态,app正在运行且不会被杀死
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND: // 系统正处于低内存状态,app进程位于LRU List开始附近,尽管app进程被杀死的概率低,系统可能已经开始杀在LRU List中的后台进程,此时应该释放一些资源,防止被杀的机率
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW: // 系统处于更低内存状态,app正在运行且不会被杀死,可以释放不用的资源提升app性能
ComponentCallbacks2.TRIM_MEMORY_MODERATE: //  系统正处于低内存状态,app进程位于LRU List中部附近,如果系统进一步内存紧张,app进程可能会被杀掉
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL: // 系统处于极低的内存状态,app仍然不会被杀死。但如果不是放资源,系统开始杀后台进程,此时app应该清理一些资源
ComponentCallbacks2.TRIM_MEMORY_COMPLETE: // 系统正处于低内存状态,app进程是首先被杀进程之一,如果系统现在没有恢复内存,应该立即释放对app不重要的所有内容

RecyclerViewPool 注意事项!!!

我在测试代码里面 共用了一个 RecyclerViewPool 。

// 这里需要,因为使用 RecyclerViewPool,是以空间换时间,
// 需要注意内存情况,在内存低的情况,可以尝试清理掉.
int max = 8; 
mRecycledViewPool.setMaxRecycledViews(TVUICommonItemView.ITEM_TYPE_1, max);
mRecycledViewPool.setMaxRecycledViews(TVUICommonItemView.ITEM_TYPE_2, max);
mRecycledViewPool.setMaxRecycledViews(TVUICommonItemView.ITEM_TYPE_3, max);
mRecycledViewPool.setMaxRecycledViews(TVUICommonItemView.ITEM_TYPE_4, max);
mRecycledViewPool.setMaxRecycledViews(TVUICommonItemView.ITEM_TYPE_5, max);

共用 RecyclerViewPool 注意几个问题
在这里插入图片描述
类型(Type)为0 的ItemView 还是在调用 onCreateViewHolder 的,一共调用了12次.
因为并没有使用 Pool里面的缓存,为何,里面没有东西哈,取不出来holder = getRecycledViewPool().getRecycledView(type);,holder 为 null。所以第一次显示界面,还是在拼命的创建,这里让人很沮丧!!!
在这里插入图片描述
所以结论就是,虽然共用了RecyclerViewPool ,第一次加载页面 或者 页面快速滚动 又或者 想使用缓存Pool的时候,如果Pool里面没有,就需要重新创建。

反观使用 Leanback 虽然也是共用 RecyclerViewPool,但 在滚动的过程中,如果之前的 Pool没有缓存对应的类型,也是白塔,还是需要创建大量的ItemView,反正是挺耗时的.

当有多个页面的切换的时候,如果另一个页面的 RecyclerView被释放掉了. 它页面的之前的View会被清理掉. 下次再创建这个页面,进入,如果没有缓存使用,还是会进行创建.
在这里插入图片描述

再前面 我们已经分析过,当离开屏幕的Item会保存(putRecycledView)到对应的 类型下的缓存Pool(mScrap)里面。

如何加速第一次加载的优化
这里也是用空间换时间,避免 创建视图耗费的时间,进而可以快速的显示界面.
提前创建缓冲池的 ViewHolder,这样可以减少 onCreateViewHolder,但是还是需要 onBindViewHolder

// 看个简单的小栗子
recycledViewPool.putRecycledView(adapter.createViewHolder(recyclerView, TYPE_1));
recycledViewPool.putRecycledView(adapter.createViewHolder(recyclerView, TYPE_1));
recycledViewPool.putRecycledView(adapter.createViewHolder(recyclerView, TYPE_2));

第一次进入界面会连续调用 onCreateViewHolder,因为 onCreateViewHolder没有办法去设置 ItemType,所以需要调用 createViewHolder

注意getItemViewType
当很多不同类型的控件的时候,不设置 ItemType返回的话,或者只返回 position,界面肯定会错乱,也达不到优化的好处.

在 Adapter 的 getItemViewType 设置对应的类型
@Override
public int getItemViewType(int position) {
	return mDatas.get(position).getItemType();
}

因为缓存里面会根据 Type 去取:holder = getRecycledViewPool().getRecycledView(type);
所以添加缓存的大小以及类型的也需要注意,和你的不同类型的控件记得要对应上。

mRecycledViewPool.setMaxRecycledViews(ItemView.TYPE_1, 10);

注意getItemId的使用
调用 notifyDataSetChanged 的时候,recyclerView 不知道到底发生了什么,所以它只能认为所有的东西都发生了变化,将所有的 viewHolder 都放入到 pool 中。

位置固定,比如,广告位。
不会改变
数量合理,保存在内存中没啥关系。

// Adapter.setHasStableIds(true);
// mAdapter.hasStableIds()
// 当设置了以上的 开关后,detachAndScrapAttachedViews -> scrapOrRecycleView 的 recycler.scrapView(view); 
// 将缓存到 mAttachedScrap,最后直接使用,不需要创建,绑定数据,非常优化喔!!
public long getItemId(int position) {
    Data data = datas.get(position);
    return (data.getTitle()).hashCode();
}
// 当你的标题栏被修改了,调用 notifydatasetchanged 刷新.
// 如果没有被更新,就会使用缓存,相当的优化.

// 或者一般这样写
 @Override
public long getItemId(int position) {
	return position;
}

思考:如果 getScrapOrCachedViewForId 去 获取(getItemId )的 Id 被改变了,那么 tryGetViewHolderForPositionByDeadline 接下来如何执行?自己思考下?然后单独

onViewRecycled的使用
View 被回收的时候调用,所以在这个函数里面可以释放资源;
比如释放一些图片资源,Glid,ImageView等等.

思考:为何这里可以释放掉不要的东西?
答:因为 addViewHolderToRecycledViewPool -> dispatchViewRecycled -> mAdapter.onViewRecycled,所以 ViewHolder 加入了 RecycledViewPool,复用的时候,不需要创建View,但是需要重新绑定数据.

CacheView使用注意
CacheView 默认为2个,主要是用来保存临时滚动屏幕外的 ViewHolder,主要是为了避免内存抖动.
比如你上下滚动又回来啦,只是短暂移除屏幕,又马上使用了,这里就不需要重新 创建 与 绑定.
CacheView 如果需要使用,其实就是 用空间换时间. 因为是实实在在的存在数组里面的.

RecyclerView的单击事件如何加?

一般人都会将单击事件加在 onBindViewHolder里面,这里是错误的行为!因为我们分析过 CacheView默认只有两个,如果新出的View能用到这个缓存,当然不用重新的 创建,绑定;但是大部分时间都是在使用 RecyclerPool,所以需要绑定数据,这就导致,频繁的 单击事件添加,内存抖动,这里处理的方案是,在 ViewHolder里面添加。

public static class ColorViewHolder extends RecyclerView.ViewHolder {
        public ColorViewHolder(View itemView) {
            super(itemView);
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Toast.makeText(v.getContext(), getLayoutPosition(), Toast.LENGTH_LONG).show();
                }
            });
        }

LinearLayoutMaanager.setInitialPrefetchItemCount()

RecyclerView.setHasFixedSize(true)
局部刷新,避免 false 会触发,requestLayut()会重新走三大流程(onMeasure,onLayout,onDraw)
在这里插入图片描述

多个RecyclerView公用 RecyclerViewPool

根据设备的内存,CPU情况自动设定优化策略


参考资料

自定义LayoutManger,可以了解这篇文章
http://wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1/

深入浅出RecyclerView
https://kymjs.com/code/2016/07/10/01/

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://github.com/MicroKibaco/CrazyDailyQuestion/issues/7

真正带你搞懂 RecyclerView 的缓存机制
https://zhuanlan.zhihu.com/p/80475040

RecyclerView-LayoutManger相关
RecyclerView解析之LinearLayoutManager

结束语

由于本人技术水平有限,有问题的地方还望一起探讨,学习,互相 进步,谢谢.
不要忘记三连,点赞,关注,收藏.
更欢迎加入TV社区, QQ群 与 关注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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章