RecyclerView加载了那么多图,为什么就是不崩呢?
/ 今日科技快讯 /
近日,有媒体近期透露,京东将在香港进行二次上市,计划于5月底开始新股申购。在今年4月底,有香港媒体援引路透社报道称,京东已以保密形式在香港提交上市申请,拟二次上市。京东可能出售至多约5%的股份,在香港上市大约融资34亿美元,预计最早在6月上市,其中美银、中信里昂及瑞银为主要安排行。
/ 作者简介 /
大家周一好,新的一周要保持好的心情哦~
本篇文章转载自TimLin的博客,分享了他对RecyclerView缓存机制的相关理解,希望对大家有所帮助。
原文地址:
https://juejin.im/post/5eae33a26fb9a043586c7f19
/ 前言 /
使用 ScrollView 的时候,它的所有子 view 都会一次性被加载出来。而正确使用 RecyclerView 可以做到按需加载,按需绑定,并实现复用。本文主要分析 RecyclerView 缓存复用的原理。
/ 从缓存获取ViewHolder流程概览 /
从缓存获取的大致流程如下图所示:
说明:
在创建ViewHolder之前,RecyclerView会先从缓存中尝试获取是否有符合要求的ViewHolder,详见Recycler#tryGetViewHolderForPositionByDeadline方法。
第一次
尝试从 mChangedScrap 中获取。只有在 mState.isPreLayout() 为 true 时,也就是预布局阶段,才会做这次尝试。「预布局」的概念会在介绍。
第二次
getScrapOrHiddenOrCachedHolderForPosition()获得ViewHolder。 尝试从
mAttachedScrap
mHiddenViews
mCachedViews中查找ViewHolder
其中mAttachedScrap和mCachedViews都是Recycler的成员变量。如果成功获得ViewHolder则检验其有效性,若检验失败则将其回收到RecyclerViewPool中 检验成功可以直接使用。
第三次
如果给Adapter设置了stableId,调用getScrapOrCachedViewForId尝试获取ViewHolder。跟第二次的区别在于,之前是根据position查找,现在是根据id查找。
第四次
mViewCacheExtension不为空的话,则调用ViewCacheExtension#getViewForPositionAndType方法尝试获取View。
注:ViewCacheExtension是由开发者设置的,默认情况下为空,一般我们也不会设置。这层缓存大部分情况下可以忽略。
第五次
尝试从RecyclerViewPool中获取,相比较于mCachedViews,从mRecyclerPool中成功获取 ViewHolder 对象后并没有做合法性和item位置校验,只检验viewType是否一致。从RecyclerViewPool中取出来的ViewHolder需要重新执行bind才能使用。
/ 问题 /
预布局、预测动画是什么?
理解「预布局」需要先了解「预测动画」。考虑这样一个场景:用户有A、B、C三个item,A,B刚好显示在屏幕中,这个时候,用户把B删除了,那么最终C会显示在B原来的位置。
如果C从底部平滑地滑动到之前B的位置将会更符合直觉。但是要做到这点实际上没那么简单。因为我们只知道C最终的位置,但是不知道C的起始位置在哪里,无法确定C应该从哪里滑动过来。如果根据最终的状态,就断定C应该要从底部滑动过来的话,很可能是有问题的。因为在其他LayoutManager中,它可能是从侧面或者是其他地方滑动过来的。
那根据原状态与最终状态之间的差异,能不能得出我们应该执行什么样的切换动画呢?答案依然是no。因为在原状态中,C根本就不存在。(这个时候,我们并不知道,B要被删除了,如果把C给加载出来,很可能是一种资源浪费。)
设计RecyclerView的工程师是这么解决的。当Adapter发生变化的时候,RecyclerView会让LayoutManager进行两次布局。
第一次是预布局。将之前原状态下的item都布局出来。并且根据Adapter的notify信息,我们知道哪些item即将变化了,所以可以加载出另外的View。在上述例子中,因为知道B已经被删除了,所以可以把屏幕之外的C也加载出来。
第二个,最终的布局,也就是变化完成之后的布局。
这样只要比较前后布局的变化,就能得出应该执行什么动画了。这种负责执行动画的 view 在原布局或新布局中不存在的动画,就称为预测动画。预布局是实现预测动画的一个步骤。下面两个动图展示了普通动画与预测动画效果的区别:
普通动画
预测动画
关于Scrap
Scrap缓存列表(mChangedScrap、mAttachedScrap)是RecyclerView最先查找ViewHolder地方,它跟RecyclerViewPool或者ViewCache有很大的区别。
mChangedScrap和mAttachedScrap只在布局阶段使用。其他时候它们是空的。布局完成之后,这两个缓存中的viewHolder,会移到mCacheView或者 RecyclerViewPool中。
当LayoutManager开始布局的时候(预布局或者是最终布局),当前布局中的所有view,都会被dump到scrap中(具体实现可见LinearLayoutManager#onLayoutChildren()方法中调用了detachAndScrapAttachedViews() ),然后LayoutManager挨个地取回view,除非view发生了什么变化,否则它会马上从scrap中回到原来的位置。
以上图为例,我们删除掉b,调用notifyItemRemove方法,触发重新布局,这时a,b,c 都会被dump到scrap中,然后LayoutManager会从scrap中取回a和c。
偏个题,这个时候,b去哪了?RecyclerView看到b没有出现在最终的布局中,会unscrap它,让它执行一个消失的动画然后隐藏。动画执行完之后,b被放到RecyclerViewPool中。
为什么LayoutManager需要先执行detach,然后再重新attach这些view,而不是只移除哪些变化的子view呢?Scrap缓存列表的存在,是为了隔离LayoutManager和RecyclerView.Recycler之间的关注点/职责。LayoutManager不需要知道哪一个子view应该保留 或者是 应该被回收到pool亦或者其他什么地方。这是Recycler的职责。
除了在布局时不为空外,还有另一个与scrap有关的规律:所有scrap的 view 都会跟RecyclerView分离。ViewGroup中的attachView和detachView方法跟addView和removeView方法很像,但是不会触发请求布局会重绘的事件。它们只是从ViewGroup的子view列表中删除对应的子view,并将该子view的parent设置为null。detached状态必须是临时,后面紧随着attach或者remove事件。
如果在计算一个新布局的时候,已经添加了一堆子view,可以放心的将它们全部detach ,Recyclerview就是这么做的。
Attached vs Changed scrap
Recycler类中,我们可以看到两个单独的scrap容器:mAttachedScrap和mChangedScrap。为什么需要两个呢?
ViewHolder只有在满足下面情况才会被添加到mChangedScrap:当它关联的item发生了变化(notifyItemChanged或者notifyItemRangeChanged被调用),并且ItemAnimator调用ViewHolder#canReuseUpdatedViewHolder方法时,返回了false。否则,ViewHolder会被添加到AttachedScrap中。
canReuseUpdatedViewHolder返回“false”表示我们要执行用一个view替换另一个view的动画,例如淡入淡出动画。“true”表示动画在view内部发生。
mAttachedScrap在整个布局过程中都能使用,但是changed scrap——只能在预布局阶段使用。
这是有道理的:在布局后,新的ViewHolder应该替换掉“改变了的”视图,因此AttachedScrap在布局后是没有用的。更改动画执行完成后,change scrap将按预期方式转存到pool中。默认的ItemAnimator可以在3种情况下重用更新的ViewHolder:
调用了setSupportsChangeAnimations(false)。
调用了notifyDataSetChanged而不是notifyItemChanged或notifyItemRangeChanged。
提供了这样的更改payload:adapter.notifyItemChanged(index,anyObject)。
最后一种情况显示了一种很好的方法,当只想更改一些内部元素时,可以避免创建/绑定新的ViewHolder。
Hidden Views是什么?
前面提到在第二次尝试获取ViewHolder的时候,有一个子步骤会从hidden view中搜索,这里的hidden view指的是什么?「hidden view」指的是那些正在从RecyclerView边界中脱离的view。为了让这些view正确地执行对应的分离动画,它们仍然作为RecyclerView的子view被保留下来。
站在LayoutManager的角度,这些view已经不存在了,因此不应该被包含在计算里面。比如 在部分view正在执行消失动画的过程中,调用 LayoutManager#getChildAt方法,这些view不算在下标里面。来自LayoutManager的所有对getChildAt()、getChildCount()、addView()等的方法调用。在应用到实际的可回收view之前,都要通过ChildHelper处理,ChildHelper的职责是重新计算非隐藏的子view列表和完整的子view列表之间的索引。
请记住,我们正在搜索要提供给LayoutManager的视图,但是LayoutManager不应了解隐藏View!
举一个实际的🌰:这种让人费解的“从隐藏的view弹跳”(bouncing from hidden views)机制对于处理下面这种情况而言是很有必要的。考虑这种场景,我们插入一个item ,然后在插入动画完成之前,马上删除该item:
我们想要看到的是b从c移除时的位置开始向上平移。但是在那个时候,b是一个隐藏的view!如果我们忽略了它(“隐藏”的b),那会导致在现有b下面创建一个新的b。更糟糕的是,这两个view会重叠,因为新的b会往上,旧的b会往下。为了避免这种错误,在搜索ViewHolder的较早步骤之一中,RecyclerView会询问ChildHelper是否具有合适的hidden view。所谓「合适」,表示这个view跟我们需要的位置相关联,并具有正确的view type,并且这个view的被隐藏的原因不是为了移除掉它(我们不应该让被移除的view复活)。
如果有这样的view ,RecyclerView会将其返回到LayoutManager并将其添加到preLayout中以标记应从其进行动画处理的位置(详见recordAnimationInfoIfBouncedHiddenView方法)。
什么?在布局前后添加内容不应该是LayoutManager的职责吗?怎么现在RecyclerView也在往preLayout中添加view?是的,这种机制看起来有点职责部分,但这是也说明我们有必要了解它。
Stable Id的作用是什么?
理解stable Id特性的最重要的一个点是,它只会在调用notifyDataSetChanged方法之后,影响RecyclerView的行为。
如果调用notifyDataSetChanged的时候,Adapter并没有设置hasStableId,RecyclerView不知道 发生了什么,哪一些东西变化了,所以,它假设所有的东西都变了,每一个ViewHolder都是无效的,因此应该把它们放到RecyclerViewPool而不是scrap中。
如果有Stable Id,那那将会是像下面这样:
ViewHolder会进入scrap而不是pool中。然后会通过特定的Id(Adapter中的getItemId获取到的id)而不是postion到scrap中查找ViewHolder。
好处是什么?不会导致RecyclerViewPool溢出,因此非必须情况下,不需要创建新的ViewHolder。之前的ViewHolder会重新绑定,因为Id没有变化不代表内容没有变化。
最大好处的好处是支持动画。上面移动item4到item6的位置。正常情况下,我们需要调用notifyItemMoved(4,6)才能得到一个移动动画。但是通过stable id,调用notifyDataSetChanged也能支持这一点。因为RecyclerView可以看到特定id的view在新旧布局的上的位置,要注意的是,这里的动画只支持简单的动画,预测动画无法支持。如果我们在新布局中看到一些ID,而在旧布局中没有,那么我们如何知道它是新插入的item还是从某处移入的item,在后一种情况下它究竟是从哪里来的呢?通常,这些问题的答案会在预布局中找到,根据适配器的更改,该布局已超出RecyclerView的范围,但现在这种情况下, 我们不知道这些更改具体是什么。
总体而言,stable id的使用场景似乎比较有限。不过,还是有这样一个使用场景:如果是从ListView迁移到RecyclerView,将所有notifyDataSetChanged调用,都转换为特定更改的通知可能会很痛苦。在这种情况下,stable id可以提供给你提供简单的RecyclerView动画。
/ 缓存优化实践 /
尽量使用notifyItemXxx方法进行细粒度的通知更新,而不是notifyDatasetChanged。如果变更前后是两个数据集,无法确定具体哪一些数据项变化了,可以考虑使用DiffUtil 。如果数据集较大,建议结合使用AsyncListDiffer在子线程做diff运算。
如果特定viewType的item只有一个,可以通过RecyclerView#getRecycledViewPool()#setMaxRecycledViews(viewType,1); 来调整缓存区的大小,减少内存占用。
如果特定viewType的item特别多,但是不得不通过notifyDataSetChange方法更新数据,可以通过下面这种方式,在变更前调大缓存,变更完成后,调小缓存。这样布局变化也可以最大程度地复用已有的ViewHolder。
mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 屏幕显示的item总数+7 );
mAdapter.notifyDataSetChanged();
new Handler().post(new Runnable() {
@Override
public void run() {
mRecyclerView.getRecycledViewPool()
.setMaxRecycledViews(0, 5);
}
});
如果RecyclerView中的每个item都是一个RecyclerView, 并且子RecyclerView的item type相同可以通过 RecyclerView#setRecycledViewPool(); 方法,实现缓存池的复用。
推荐阅读:
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注