RecyclerView缓存机制与性能优化

ViewHolder的属性

View itemView:对应RecyclerView的子View

int mPosition: View当前对应数据在数据源中的位置

int mOldPosition: View上次绑定的数据在数据源中的位置

long mItemId:可以判断ViewHolder是否需要重新绑定数据

int mItemViewType:itemView对应的类型

int mPreLayoutPosition: 在预布局阶段ViewHolder对应数据在数据源中的位置

int mFlags:ViewHolder对应的标记位

List<Ojbect> mPayloads:实现局部刷新

Recycler mScrapContainer:如果不为空,表示ViewHolder是存放在Scrap缓存中

RecyclerView 为了做ViewHolder动画,对ViewHolder做了 2 次布局,第一次叫预布局pre-layout,第二次叫后布局post-layout

mAttachedScrap:没有容量限制,缓存的ViewHolder是不做修改的,不会重新走Adapter的绑定方法

作用是解藕LayoutManager 和 RecyclerView.Recycler 之间的职责所在

mChangedScrap:没有容量限制,缓存的ViewHolder是做修改的,重新走Adapter的绑定方法

作用是让变化的ViewHolder执行动画时更加顺滑

mCachedViews:有容量限制,默认是2,缓存的ViewHolder是被dettach掉的,缓存的ViewHolder依然保存dettach前的数据,如position,绑定数据等,不会重新走Adapter的绑定方法

作用是保证快速滑动时获取ViewHolder时不走Adapter的绑定方法

mRecyclerPool:有容量限制,默认是每个itemType缓存5个ViewHolder,缓存的ViewHolder是被dettach掉的,缓存的ViewHolder是恢复出厂设置的,需要重新走Adapter的绑定方法,根据itemType来分开存储的, 可以提供给多个RecyclerView共享

mViewCacheExtension:留给开发者自由发挥的,官方并没有默认实现

mHiddenViews:不在Recycler类中,在ChildHelper类,缓存正在从 RecyclerView 边界中脱离的 ViewHolder,ChildHelper 的职责是重新计算非隐藏的子 view 列表和完整的子 view 列表之间的索引

作用是为了让脱离的ViewHolder 正确地执行对应的分离动画

RecyclerView从无到有的加载过程

如何获取一个ViewHolder?

第一步,先判断当前布局状态是否为预布局状态,一般在调用notifyItemChanged方法或数据变化时为预布局状态,先从mChangedScrap中获取已经被detach掉的viewholder进行重新绑定新的数据

第二步,如果没有找到viewholder,再从Scrap,Hidden,Cached三处缓存中获取viewholder,先是mAttachedScrap,其次mHiddenViews,最后mCachedViews

第三步,mViewCacheExtension中获取,默认是null,由开发者自定义的缓存策略,一般不会特别自定义过缓存策略,所以这里也获取不到viewholder

第四步,RecyclerPool中获取,通过itemType从SparseArray类型的ScrapData,再获取到viewholder,需要重新对viewholder绑定

第五步,调用了mAdapter. onCreateViewHolder来创建一个新的viewholder

注意:当第一次onLayoutChildren时还没有任何viewholder,所有的viewholder都需要onCreateViewHolder创建。

RecyclerView增删viewholder的过程

RecyclerView 为了做增删viewholder的平滑动画,对表项做了 2 次布局,一次是预布局,一次是实际布局

列表中有两个表项(1、2),删除 2,此时 3 会从屏幕底部平滑地移入并占据原来 2 的位置。

为了实现该效果,RecyclerView的策略是:为动画前的表项先执行一次pre-layout,将不可见的表项 3 也加载到布局中,形成一张布局快照(1、2、3)。再为动画后的表项执行一次post-layout,同样形成一张布局快照(1、3)。比对两张快照中表项 3 的位置,就知道它该如何做动画了。

如果支持viewholder动画,则 onLayoutChildren() 就会被调用 2 次

RecyclerView 在预布局阶段准备向列表中填充ViewHolder前,会清空现有的ViewHolder 1、2,把它们都 detach 并回收对应的 ViewHolder 到 mAttachedScrap列表中。

RecyclerView滑动时的缓存过程

先处理被滚动出去的viewholder,会把viewholder remove掉而不是detach掉,remove的作用是保留绑定关系,数据等,重新获取时不用重新绑定,然后将remove掉的viewholder判断是否满足缓存在mCachedViews的条件,如果满足,就判断mCachedViews是否已经满了,如果满了的话就会将缓存中最老的viewholder转移到RecyclerPool中,再将需要缓存的viewholder缓存进mCachedViews中,如果不满足,直接被放进RecyclerPool中

再处理被滚动进来的viewholder,实际还是在获取一个ViewHolder的流程,但是因为在布局完成之后,Scrap层的缓存就是空的了,所以只能从mCachedViews或者RecyclerPool中获取viewholder了,都取不到最后就会走onCreateViewHolder创建视图

RecyclerView数据更新时的缓存过程

在调用notifyDataSetChanged方法后,所有的子view会被标记,这个标记导致它们最后都被缓存到RecyclerPool中,然后重新绑定数据。并且由于RecyclerPool有容量限制,如果不够最后就要重新创建新的视图了。

但是使用notifyItemChanged等方法会将视图缓存到mChangedScrap和mAttachedScrap中,这两个缓存是没有容量限制的,所以基本不会重新创建新的视图,只是mChangedScrap中的视图需要重新绑定一下。

预布局过程中从mChangedScrap缓存中获取ViewHolder。获取逻辑如下:

线性遍历 mChangedScrap,position == ViewHolder.mPreLayoutPosition,返回该ViewHolder,否则走逻辑2

Adapter.hasStableIds()返回false,返回null,否则走逻辑3

线性遍历 mChangedScrap,mAdapter.getItemId(offsetPosition) == holder.getItemId,返回该ViewHolder,否则走逻辑4

上述都没有获取到,返回null

Stable Id 的作用是什么?

只会在调用 notifyDataSetChanged 方法之后,影响 RecyclerView 的行为。

如果调用 notifyDataSetChanged 的时候,Adapter 并没有设置 hasStableId,RecyclerView 不知道 发生了什么,哪一些东西变化了,所以,它假设所有的东西都变了,每一个 ViewHolder 都是无效的,因此应该把它们放到 RecyclerViewPool 而不是 scrap 中。

Adapter 设置了 StableId,ViewHolder 会进入 scrap 而不是 pool 中。然后会通过特定的 Id(Adapter 中的 getItemId 获取到的 id)而不是 postion 到 scrap 中查找 ViewHolder。

RecyclerView缓存优化实践

尽量使用 notifyItemXxx 局部更新方法进行细粒度的通知更新,而不是 notifyDatasetChanged

如果变更前后是两个数据集,无法确定具体哪一些数据项变化了,可以考虑使用DiffUtil

如果数据集较大,建议结合使用AsyncListDiffer在子线程做 diff 运算。

如果特定 viewType 的 item 只有一个,可以通过 RecyclerView#getRecycledViewPool()#setMaxRecycledViews(viewType,1); 来调整缓存区的大小,减少内存占用

如果 RecyclerView 中的每个 item 都是一个 RecyclerView, 并且子 RecyclerView 的 item type 相同可以通过 RecyclerView#setRecycledViewPool(); 方法,实现缓存池的复用

RecyclerView数据处理上:

数据处理与视图绑定分离

加大RecyclerView的缓存

用空间换时间,来提高滚动的流畅性。

recyclerView.setItemViewCacheSize(20);

recyclerView.setDrawingCacheEnabled(true);

recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);

减少过度绘制

设置高度固定

可以使用RecyclerView.setHasFixedSize(true);来避免requestLayout浪费资源。

扁平化布局,减少view对象的创建

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

推荐阅读更多精彩内容