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。 尝试从

  1. mAttachedScrap 

  2. mHiddenViews 

  3. 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(); 方法,实现缓存池的复用。 

推荐阅读:

这本《第三行代码》,让大家久等了!

这么炫酷的动画效果,你还在等什么?

Android 11来了,快!扶我起来

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值