使用 ScrollView 的时候,它的全部子 view 都会一次性被加载出来。而正确使用 RecyclerView 能够作到按需加载,按需绑定,并实现复用。本文主要分析 RecyclerView 缓存复用的原理。java
从缓存获取的大体流程以下图所示:android
说明:git
在建立 ViewHolder 以前,RecyclerView 会先从缓存中尝试获取是否有符合要求的 ViewHolder,详见 Recycler#tryGetViewHolderForPositionByDeadline
方法github
理解「预布局」须要先了解「预测动画」。考虑这样一个场景:缓存
用户有 A、B、C 三个 item,A,B 恰好显示在屏幕中,这个时候,用户把 B 删除了,那么最终 C 会显示在 B 原来的位置ide
若是 C 从底部平滑地滑动到以前 B 的位置将会更符合直觉。可是要作到这点实际上没那么简单。由于咱们只知道 C 最终的位置,可是不知道 C 的起始位置在哪里,没法肯定 C 应该从哪里滑动过来。若是根据最终的状态,就判定 C 应该要从底部滑动过来的话,极可能是有问题的。由于在其余 LayoutManager 中,它多是从侧面或者是其余地方滑动过来的。布局
那根据原状态与最终状态之间的差别,能不能得出咱们应该执行什么样的切换动画呢?答案依然是 no。由于在原状态中,C 根本就不存在。(这个时候,咱们并不知道,B 要被删除了,若是把 C 给加载出来,极可能是一种资源浪费。)post
设计 RecyclerView 的工程师是这么解决的。当 Adapter 发生变化的时候,RecyclerView 会让 LayoutManager 进行两次布局。学习
这样只要比较先后布局的变化,就能得出应该执行什么动画了。优化
这种负责执行动画的 view 在原布局或新布局中不存在的动画,就称为预测动画。
预布局是实现预测动画的一个步骤。
下面两个动图展现了普通动画与预测动画效果的区别:
普通动画 👇
预测动画 👇
关于预测动画,感兴趣的同窗能够进一步阅读这篇文章。
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 就是这么作的。
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:
最后一种状况显示了一种很好的方法,当只想更改一些内部元素时,能够避免建立/绑定新的 ViewHolder。
前面提到在第二次尝试获取 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 特性的最重要的一个点是,它只会在调用 notifyDataSetChanged 方法以后,影响 RecyclerView 的行为。
若是调用 notifyDataSetChanged 的时候,Adapter 并无设置 hasStableId,RecyclerView 不知道 发生了什么,哪一些东西变化了,因此,它假设全部的东西都变了,每个 ViewHolder 都是无效的,所以应该把它们放到 RecyclerViewPool 而不是 scrap 中。
若是有 Stable Id,那那将会是像下面这样:
ViewHolder 会进入 scrap 而不是 pool 中。而后会经过特定的 Id(Adapter 中的 getItemId 获取到的 id)而不是 postion 到 scrap 中查找 ViewHolder。
好处是什么?
整体而言,stable id 的使用场景彷佛比较有限。 不过,仍是有这样一个使用场景:若是是从 ListView 迁移到 RecyclerView,将全部 notifyDataSetChanged 调用,都转换为特定更改的通知可能会很痛苦。 在这种状况下,stable id 能够提供给你提供简单的 RecyclerView 动画。
尽可能使用 notifyItemXxx 方法进行细粒度的通知更新,而不是 notifyDatasetChanged
若是特定 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(); 方法,实现缓存池的复用。
因为本人水平有限,可能出于误解或者笔误不免出错,若是发现有问题或者对文中内容存在疑问请在下面评论区告诉我,谢谢!