看完这篇,面试RecyclerView的时候再也不怕了
/ 今日科技快讯 /
6月26日,法拉第未来解雇了数十名停薪留职员工,其中包括该公司位于加州汉福德工厂的制造工人。据悉,这些员工于2018年底开始停薪留职,期间延长了一次停薪留职时间并能够获得相应岗位福利。根据一名前雇员提供的解雇文件,这些最新被解雇的员工福利将于6月30日到期。
/ 作者简介 /
本篇文章来自lyldalek的投稿,分享了他对RecyclerView的缓存机制的理解,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
lyldalek的博客地址:
https://blog.csdn.net/weixin_43130724
/ 前言 /
以后会尽量写非源码的东西,因为过一段时间之后,发现自己看自己的文章都费劲,而且,源码大部分是在分析方法的调用链,意义不大,需要多写写自己的体悟才行。如果有时候必须要写,也要简化调用链的分析,提供类图,写重点和结论。
本文的思路与结论是基于参考文档的Anatomy of RecyclerView: a Search for a ViewHolder
https://android.jlelse.eu/anatomy-of-recyclerview-part-1-a-search-for-a-viewholder-404ba3453714
与 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 /
RecyclerView 有五虎上将:
RecyclerView的职责就是将Datas中的数据以一定的规则展示在它的上面,但说破天RecyclerView只是一个ViewGroup,它只认识View,不清楚Data数据的具体结构,所以两个陌生人之间想构建通话,我们很容易想到适配器模式,因此,RecyclerView需要一个Adapter来与Datas进行交流:
如上如所示,RecyclerView表示只会和ViewHolder进行接触,而Adapter的工作就是将Data转换为RecyclerView认识的ViewHolder,因此RecyclerView就间接地认识了Datas。
事情虽然进展愉快,但RecyclerView是个很懒惰的人,尽管Adapter已经将Datas转换为RecyclerView所熟知的View,但RecyclerView并不想自己管理些子View,因此,它雇佣了一个叫做LayoutManager的大祭司来帮其完成布局,现在,图示变成下面这样:
如上图所示,LayoutManager协助RecyclerView来完成布局。但LayoutManager这个大祭司也有弱点,就是它只知道如何将一个一个的View布局在RecyclerView上,但它并不懂得如何管理这些View,如果大祭司肆无忌惮的玩弄View的话肯定会出事情。
所以,必须有个管理View的护法,它就是Recycler,LayoutManager在需要View的时候回向护法进行索取,当LayoutManager不需要View(试图滑出)的时候,就直接将废弃的View丢给Recycler,图示如下:
到了这里,有负责翻译数据的Adapter,有负责布局的LayoutManager,有负责管理View的Recycler,一切都很完美,但RecyclerView乃何等神也,它下令说当子View变动的时候姿态要优雅(动画),所以用雇佣了一个舞者ItemAnimator,因此,舞者也进入了这个图示:
如上,我们就是从宏观层面来对RecylerView有个大致的了解,可以看到,RecyclerView作为一个View,它只负责接受用户的各种讯息,然后将信息各司其职的分发出去。
还有最后一个 ,ItemDecoration 是为了显示每个 item 之间分隔样式的。它的本质实际上就是一个 Drawable。当 RecyclerView 执行到 onDraw() 方法的时候,就会调用到他的 onDraw(),这时,如果你重写了这个方法,就相当于是直接在 RecyclerView 上画了一个 Drawable 表现的东西。
而最后,在他的内部还有一个叫getItemOffsets()的方法,从字面就可以理解,他是用来偏移每个 item 视图的。当我们在每个 item 视图之间强行插入绘画了一段 Drawable,那么如果再照着原本的逻辑去绘 item 视图,就会覆盖掉 Decoration 了,所以需要getItemOffsets()这个方法,让每个 item 往后面偏移一点,不要覆盖到之前画上的分隔样式了。
PS:
其实 ItemDecoration 的宽高是计算在 itemview 中的,只不过 itemview 本身绘制区域没有那么大,留出来的地方正好的透明的,于是就透过 itemview 显示出了 ItemDecoration。那么就很有意思了,如果我故意在 ItemDecoration 的偏移量中写成0,那么 itemview 就会挡住 ItemDecoration,而在 itemview 的增加或删除的时候,会短暂的消失(透明),这时候就又可以透过 itemview 看到 ItemDecoration 的样子。使用这种组合还可以做出意想不到的动画效果。
虽然,Google 尽力的解耦了,但是在源码中有的地方还是会有逻辑混杂在一起的地方,比如:动画的处理。
为了能够更好的理解下面的内容,这里先介绍一下 pre-layout 与 post-layout 是什么。
有这样的一个场景:我们有3个item【a, b, c】,其中a与b显示在屏幕上,当我们删除b的时候,c会显示出来。
我们希望看到的是 c 从底部顺利滑动到它的新位置。
但这是如何发生呢?
我们知道新布局中 c 的最终位置,但我们如何知道它应该从何处开始滑动?
谷歌的解决方案提供如下:
在adapter发生更改后,RecyclerView会从LayoutManager请求两个布局。
第一个 —— pre-layout,因为我们可以收到适配器的变化,所以这里我们可以做一些特殊的处理。在我们的例子中,因为我们现在知道b被删除了,所以我们会额外的显示出c,尽管它已经超出界限。
第二个 —— post-layout,一个正常的布局,对应于更改后的适配器状态。
现在,通过比较pre-layout和post-layout中c的位置,我们可以正确地为其设置动画。
仔细思考一下这个动画,This kind of animation — when the animated view is not present either in previous layout or in the new one — is called predictive animation。
再思考一个场景:如果b只是发生了变化,而不是被删除了,那么会怎么样呢?
答案是,仍然会在 pre-layout 阶段将 C 放置到后面!为什么呢?因为无法预测C的动画是什么动画,也许动画使b的高度变小了呢,那么c就需要显示出来,如果没有,那么 C 就会被放到缓存里去。
/ 缓存介绍 /
先简单看下代码:
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;
scrap
scrap缓存是recyclerView最先搜索的缓存,网上有很多缓存调用图,第一个缓存调用就是 scrap。scrap分为两个集合。
scrap仅仅在layout期间不为空。当LayoutManager开始 layout 的时候(pre-layout 或 post-layout),会将所有的viewHolder都放到scrap中。然后一个个再取回来,除非有些view发生了变化。
这里说一个题外话,就是有人可能会问,为啥要先放到 scrap 了之后,再取出来啊,这不是没事找事么?
我的观点是:layout 的事归LayoutManager管,缓存的事归Recycler管。LayoutManager不应该知道哪个viewHolder是否有效,这是一种职责分离设计。
mAttachedScrap和mChangedScrap:这两个比较特殊,我跟踪了一下mAttachScrap 添加的时机,在LinearLayoutManager的onLayoutChildren方法里面:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
// 这里调用了 mAttachedScrap.add(holder);
// 这里也调用了 mChangedScrap.add(holder);
detachAndScrapAttachedViews(recycler);
...
}
这里的调用时机很值得思考,它在布局的时候就放到缓存里面了,这里说明这个缓存针对的是屏幕上显示的View.
那么问题就来了,屏幕上显示的为什么要缓存起来呢?我的想法倾向于是减少 layout 方法调用带来的影响。
比如说,当我们调用notifyItemRangeChanged方法的时候,会触发requestLayout方法,就会重新布局,重新布局的话,就会先将viewHolder放到scrap中(屏幕上变化的放入mChangedScrap中,其余的放入mAttachedScrap中),然后fill布局的时候,再从 mAttachedScrap里面取出来直接使用。
mChangedScrap中的viewHolder会被移动到RecycledViewPool中,所以mChangedScrap对应的item需要从pool中取对应的viewHolder,然后重新绑定。
现在再来思考一下,为什么需要mChangedScrap与mAttachedScrap这两个缓存?
因为mChangedScrap表示item变化了,有可能是数据变化,有可能是类型变化,所以它的viewHolder无法重用,只能去RecycledViewPool中重新取对应的,然后再重新绑定。
mChangedScrap与mAttachedScrap,作用差不多。
mChangedScrap更多的用于pre-layout的动画处理。
然后一点需要注意:mChangedScrap只能在pre-layout中使用,mAttachedScrap可以在pre-layout与post-layout中使用。
在继续讨论之前,需要先说明几个方法之间的区别:
View中的detach和remove
detach在ViewGroup中的实现很简单,只是将当前View从ParentView的ChildView数组中移除,将当前View的mParent设置为null, 可以理解为轻量级的临时remove。
remove 代表真正的移除,不光从ChildView数组中移除,其他和View树各项联系也会被彻底斩断。
Recycled View中的Scrap View
Scrap View指的是在RecyclerView中,经历了detach操作的缓存。RecyclerView源码中部分代码注释的detach其实指代的是remove,此类缓存是通过position匹配的,不需要重新bindView。
Recycled View指代的就是真正的移除操作remove后的缓存,取出时需重新bindView使用。
cache 与 pool
cache 与 pool 中储存的均属于 Recycled View ,需要重新 add 到 列表中。
mCachedViews,这个比较简单。
它是一个 ArrayList 类型,不区分 viewHolder 的类型,大小限制为2,但是你可以使用 setItemViewCacheSize()这个方法调整它的大小。
由于它不区分 viewHolder 的类型,所以只能根据 position 来获取 viewHolder 。
RecycledViewPool,它储存了各个类型的 viewHolder
最大数量为5,可以通过 setMaxRecycledViews() 方法来设置每个类型储存的容量。还有一个重要的点就是,可以多个列表公用一个 RecycledViewPool,使用 setRecycledViewPool() 方法。
这里顺便说一下,各个缓存的使用上的区别,也好对各个缓存池有一个大概的了解:
如果在所有缓存中都没有找到 viewHolder,那就会调用 create 和 bind 方法。
如果在 pool (RecycledViewPool ) 中找到了,那么会调用 bind 方法。
如果在 cache (mCachedViews)中找到了,啥都不用做,直接显示就好了。
所以,需要注意他们的区别,一个 viewHolder 进入到 cache 与进入到 pool 中是不一样的。
现在,我们来思考下一个问题:mCachedViews 的大小是有限制的,如果存不下了,怎么办?
实际上,mCachedViews虽然是一个 ArrayList,但是它的工作方式却和链表有点类似。当 mCachedViews 满了之后,它会将最先存入的元素移除,放入到pool中,如下图:
当我们滑动列表的时候,一旦item超出了屏幕,那么就会被放入到mCachedViews 中,如果满了,就会将“尾部”的元素移动到pool中,如果pool也满了,那么就会被丢弃,等待回收。
下面,用几个场景来巩固一下我们学到的知识:
场景一:
先看图的左边(此时假设 cache 与 pool 中没有东西),当向下滑动时,3 最先进入 mCachedViews,随后是 4 与 5,5 会将3挤出来,3就会跑到 pool 中去了。
再看图的右边,继续向下滑动时,4 被 6 挤出来,放到了 pool 中,同时,8需要显示,那么就会先从 pool 中取,发现正好有一个 3,那么就会取出来,将 3 重新显示到屏幕上。
场景二:
如果,向下滑倒7显示出来之后,不再继续向下,而是往上滑动,那么又会怎么样呢?
看图的右边,很明显,5 从 cache 中被取出来直接复用,不用重新绑定,7 被放入了 cache 中。
思考一下,对于这种情况,我们应该如何加以利用呢?
比如,我们有一个壁纸库的列表,用户经常会上下(左右)滑动,那么我们增加 cache 的容量,就会获得更好得性能。然而对于feed流之类得列表,用户很少返回,所以增加 cache 容量意义不大。
再深入一下,我们继续向上滑动,那么,4 与 7 会放入到 cache 中,3 会从 pool 中取出来,但是,这里需要注意,因为 3 是从 pool 中取出来的,所以它需要重新绑定,但是从逻辑上来说,如果 3 位置的数据没有发生变化,它不需要重新绑定,也是有效的。所以,你也可以把这里当作一个优化点,在 onBindViewHolder() 方法中,检查一下。
再再深入一下,在我们滑动的过程中,一个类型的 viewHolder 在 pool 中应该一直只会存在一个(除非你使用了 GridLayoutManager),所以,如果你的 pool 中存在多个 viewHolder 的话,他们在滚动过程中基本上是无用的。
场景三:
当我们调用 notifyDataSetChanged() 或者 notifyItemRangeChanged(i, c) (c这个范围非常大的时候),那么很多 viewHolder 都会最终被放入到 pool 中,因为 pool 只能放置 5 个,那么多余的就会被丢弃,等待回收。最重要的是会重新 create 与 bind 对性能影响比较大。如果你的列表能够容纳很多行,而且使用 notifyDataSetChanged 方法比较频繁,那么你应该考虑设置一下容量大小。
recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 20);
ViewCacheExtension
这个是需要自定义的,而且使用有很大的限制,所以不深入介绍了。
因为它需要你自己创建 viewHolder,并将它缓存起来,那么问题就来了。当我们删除或添加一个 item 的时候,AdapterHelper 回调 RecyclerView 通知它需要处理变化。RecyclerView 会遍历当前显示的 viewHolder 然后移动它们的位置。但是这里有个bug,RecyclerView 根本不知道你创建的 viewHolder,所以它不会管你自己缓存的 viewHolder。
所以,如果你想使用的话,需要满足一些条件:
位置固定,比如,广告位。
不会改变
数量合理,保存在内存中没啥关系。
Stable Ids
之前我们说过,当调用 notifyDataSetChanged 的时候,recyclerView 不知道到底发生了什么,所以它只能认为所有的东西都发生了变化,即,将所有的 viewHolder 都放入到 pool 中。
但是,如果我们设置了 stable ids,那么就会不一样了:
viewHolder 被放入了 scrap 中,而不是 pool 中。注意,这里,它的性能提升了很多!
不用重新绑定,重新创建新的 viewHolder,不用重新 addView。addView会导致重新测量…
原来我们需要调用 notifyItemMoved(4, 6),但是现在直接调用 notifyDataSetChanged() 就好了,但是我测试的是没有动画效果的。
/ 总结 /
到了这里,你应该可以回答下面的问题了:
notifyDataSetChanged 与 notifyItemRangeChanged 的区别?
RecyclerView 与 ListView 缓存的区别?这个问题,即使你不知道 ListView 的缓存机制,也应该能说些什么。
如何对一个列表进行性能优化?调用 notifyDataSetChanged 时闪烁的原因?
推荐阅读:
ConstraintLayout已经2.0了,你不来了解一下吗?
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注