RecyclerView的剖析:搜索ViewHolder

955 阅读11分钟
帕维尔·史马科夫(Pavel Shmakov)

介绍

在本系列文章中,我将分享我对RecyclerView内部工作的知识。为什么?
试想一下:几乎每个现代Android应用程序都需要RecyclerView,因此开发人员使用它的方式会影响成千上万用户的体验。但是,我们在RecyclerView上有什么样的教学材料?
您当然可以找到有关如何使用RecyclerView的一些基本教程,但是它如何工作?
“黑匣子”方法绝对不够好,尤其是在进行复杂的自定义或优化性能时。[¹](https://android.jlelse.eu/anatomy-of-recyclerview-part-1-a-search-for-a-viewholder-404ba3453714#aa26)那里的“最深”材料可能是[RecyclerView的内部和](https://www.youtube.com/watch?v=LqBlYJTfLP4)外部我建议在Google I / O 2016上发表演讲,但我要认真地说,这甚至还不接近“出入门”,这只是冰山一角。我的目标是更进一步。我假设读者具有RecyclerView的基本知识,诸如LayoutManager之类的知识,如何将数据中的特定更改通知Adapter或如何使用项目视图类型。
在第一部分中,我们将通过RecyclerView中的一种方法来弄清楚发生了什么? `getViewByPosition() `这是源代码中最关键的部分之一,通过研究它,我们将了解RecyclerView的许多方面,例如ViewHolder回收,隐藏视图,预测动画和稳定ID。您可能会惊讶地看到这里的预测动画。好吧,事实是,尽管Google员工竭尽全力使RecyclerView的不同组件的职责脱钩,但它们之间仍然共享着许多“知识”,其中预测动画就是其中之一。没有办法避免在某一点或另一点谈论它们。
因此,在布局项目时,LayoutManager会询问RecyclerView“请在位置8给我一个View”。
这是RecycleView所做的响应:
  1. 搜索 changed scrap
  2. 搜索 attached scrap
  3. 搜索未被移除的 hidden views
  4. 搜索 view cache
  5. 如果Adapter具有稳定的ID, 通过指定ID再次搜索 attached scrapview cache
  6. 搜索 ViewCacheExtension
  7. 搜索 RecycledViewPool
如果在所有这些地方都找不到合适的View,它将通过调用adapter的`onCreateViewHolder()`方法创建一个View。然后,如果需要它将通过onBindViewHolder()绑定View,最后返回它。
如您所见,这里发生了很多事情,不仅仅是一个预期的可重用ViewHolders池。我们的目标是弄清楚所有这些缓存的含义、它们如何工作 以及为什么需要它们。我们将逐一介绍它们(以我认为最好的顺序),我们的第一站是RecycledViewPool。

RecycledViewPool

我们想回答有关每种缓存的一些问题:它的支持数据结构是什么,在什么条件下将ViewHolders存储在那里并从那里检索,最重要的是,它的目的是什么。
您可能非常了解池的用途:例如向下滚动时,顶部消失的视图将被回收到池中,以重新使用从底部出现的视图。我们将在稍后讨论ViewHolders进入池的其他场景。但是首先让我们看一下RecycledViewPool的一些代码(RecycledViewPool是RecyclerView.Recycler的内部类):
public static RecycledViewPool {
    private SparseArray <ArrayList <ViewHolder >> mScrap = new SparseArray <>();
    private SparseIntArray mMaxScrap = new SparseIntArray();
    …    
    public ViewHolder getRecycledView(int viewType){
        ArrayList <ViewHolder> scrapHeap = mScrap.get(viewType);
        …
首先,不要让名称mScrap混淆您-与上面列表中提到的**changed scrap 和 ****attached scrap**无关。我们看到每种视图类型都有自己的ViewHolders池。如果在搜索ViewHolder的过程中RecyclerView用尽了所有其他可能性,它将要求池提供具有正确视图类型的ViewHolder;那时,视图类型是唯一重要的事情。现在,每种视图类型都有其自身的功能。默认情况下为**5**,但是您可以像这样更改它:
recyclerView.getRecycledViewPool()
            .setMaxRecycledViews(SOME_VIEW_TYPE,POOL_CAPACITY);
这是非常重要的灵活性。如果屏幕上有数十个相同类型的项目,这些项目经常同时更改,请为该视图类型增大池。而且,如果您知道某些视图类型的项目非常稀有,以至于它们在屏幕上显示的数量永远不会超过一个,则请为该视图类型设置池大小1。否则,迟早池中将充满其中的5个项目,而其中4个项目只会闲置在那儿,这会浪费内存。该方法`getRecyclerView()``putRecycledView()``clear()`是公开的,所以你可以操纵池的内容。但是putRecycledView()
手动使用(例如为了事先准备一些ViewHolders)不是一个好主意。只能onCreateViewHolder()适配器的方法中创建ViewHolder,否则ViewHolders可能会RecyclerView所不希望的状态出现。
[²](https://android.jlelse.eu/anatomy-of-recyclerview-part-1-a-search-for-a-viewholder-404ba3453714#fe2a)

另一个很酷的功能是,与getter一起,getRecycledViewPool()还有一个setter  setRecycledViewPool(),因此您可以将单个池重用于多个RecycleViews。最后,我会注意到每种视图类型的池都像堆栈一样工作(后进先出)。稍后我们将了解为什么这很好。

**池化的方式**
现在让我们解决将ViewHolders扔到池中的问题。有5种情况:
  1. 在滚动过程中,视图超出了RecyclerView的范围。
  2. 数据已更改,因此视图不再可见。消失动画结束时,会添加到池中。
  3. **view cache**中的项目已更新或删除。
  4. 在搜索ViewHolder时,在**scrap**或**cache**中找到了我们想要的位置,但由于视图类型或ID错误(如果适配器具有稳定的ID),结果并不是我们想要的。 [
    ³
    ](https://android.jlelse.eu/anatomy-of-recyclerview-part-1-a-search-for-a-viewholder-404ba3453714#8b86)
  5. LayoutManager在布**pre-layout**添加了一个视图,但没有在**post-layout**添加该视图。
前两种情况非常明显。但是,要注意的一件事是,场景2不仅通过删除有问题的项目来触发,而且还通过例如插入其他项目来触发,这将给定项目推出了界限。

需要着重讨论其他情况。我们还没有介绍view cache和废料,但是场景3和4背后的想法很简单。池是我们知道“脏”的视图的地方,需要重新绑定。除池外,所有缓存中的ViewHolders保留其某些状态(最重要的是位置)。所有这些缓存均按位置进行搜索,希望某些ViewHolder可以原样重用。相反,当视图进入池时,其状态(所有标志,位置等)被清除。剩下的唯一内容是关联的视图和视图类型。众所周知,池是根据视图类型进行搜索的,当在其中找到ViewHolder时,它将重获新生。

鉴于上述情况,场景3和4不应是个谜:例如,如果我们看到**view cache**中的一项已被删除,则不再将其保存在该缓存中是没有意义的,因为它不会被重复使用。 -是相同的位置。但是,将其完全丢弃并不是很好,因此我们将其丢弃到池中。
最后一种情况要求我们知道什么是**pre-layout** 和 **post-layout**。好吧,让我们继续进行下去吧!这种情况绝对不是**pre-layout**/**post-layout**机制中最关键的方面,但是该机制通常非常重要,并且在RecyclerView的每个部分中都得到体现,因此无论如何我们都必须知道这一点。

主题:pre-layout,post-layout和predictive animations

考虑一个场景,其中我们有项目a,bc,其中ab恰好占满屏幕。我们删除b,使c出现在视图中:
![图片发布](https://miro.medium.com/max/1710/0*ZxzTdRCpireBVs4t.png)
我们希望看到的是c从底部到新位置的平滑滑动。但是那怎么可能呢?我们从新布局中知道了c的最终位置,但是我们如何知道它应该从哪里滑动呢?RecyclerView或ItemAnimator仅仅通过查看新布局认为c应该来自底部是错误的。我们可能有一些自定义LayoutManager,它可以来自侧面或别的地方。因此,我们需要来自LayoutManager的更多帮助。我们可以使用以前的布局吗?不,那里没有с在没有c的情况下b将会被删除,因此LayoutManager正确地考虑布局c是浪费资源。

Google提供的解决方案如下。适配器发生更改后,RecyclerView不是从LayoutManager请求一个布局,而是请求两个布局。第一个是pre-layout,用于布局处于先前适配器状态的项目,但是使用适配器更改来暗示布局一些额外的视图可能是一个好主意。在我们的示例中,由于我们现在知道b已被删除,因此我们另外对c进行了布局,尽管事实超出了范围。第二个布局-后布局,只是与更改后的适配器状态相对应的常规布局。

![图片发布](https://miro.medium.com/max/2358/0*8dtqyWtaehYM-7zm.)
现在,通过比较c**pre-layout****post-layout**的位置,我们可以对其外观进行适当的动画处理。这种动画-当动画视图既不在以前的布局中也不在新布局中时-称为预测动画,这是RecyclerView中最重要的概念之一。我们将在本系列的后续部分中对其进行更详细的讨论。但是,现在让我们快速看一下另一个示例:如果b被更改而不是被删除怎么办?
![图片发布](https://miro.medium.com/max/2596/0*zzI5wYYr3KdkexZ4.)
这可能令人惊讶,但是LayoutManager仍在**pre-layout**阶段对c进行布局。为什么?因为也许b的改变会使它的高度变小,谁知道呢?而且,如果b变小,则c可能会从底部弹出,因此我们最好将其布置在**pre-layout**中。但是后来,在**post-layout**,情况似乎并非如此,比如说我们只是在b内更改了一些TextView因此,不需要c,并将其扔到池中。这就是让自己进入游泳池的场景5。希望现在已经很清楚了,我们可以回到RecycledViewPool。

RecycledViewPool,继续

当我们遇到ViewHolder应该进入池中的一种情况时,那里仍然有两个障碍:它可能不可回收,并且View可能处于**Transient state**。
**可回收性**
可回收性只是ViewHolder中的一个标志,您可以使用ViewHolder类的方法`setIsRecyclable()`进行操作。RecycleView也使用它,从而使ViewHolders在动画过程中不可回收。
从不同的独立位置操作单个标志通常是一个坏主意。例如,当动画结束时,RecyclerView会调用`setIsRecyclable(true)`,因为您不希望由于特定于应用程序的某种原因而使其可回收。但是在这种情况下,事情实际上并不会中断,因为对的调用setIsRecyclable()成对的。也就是说,如果您调用setIsRecyclable(false)两次,则setIsRecyclable(true)调用一次不会使ViewHolder可回收,因此您也需要调用两次。
**Transient state**

视图的Transient state非常相似。这是View中的一个标志,由setHasTransientState()方法操作,并且调用也成对配对。View类本身不使用标志,而仅保留它。它为诸如ListView和RecyclerView之类的小部件提供了提示,最好不要在当前将此视图重用于新内容。您可以自己设置此标志,但ViewPropertyAnimator(也就是您在操作时someView.animate()…)会在动画开始自动将其设置为true,并在动画结束将其设置为false。

请注意,例如,如果使用ValueAnimator对视图进行动画处理,则必须自己管理过渡状态。
关于**Transient state**的最后一件事要注意的是,它从孩子传播到父母,一直到根视图为止。因此,如果为列表中某个项目的某些内部视图设置动画,则不仅该视图而且ViewHolder保留引用的该项目的根视图都将进入**Transient state**
**OnFailedToRecycleView**
如果要回收的ViewHolder未能通过可回收性或**Transient state**检查,则将调用适配器`onFailedToRecycleView()`方法。现在,这是非常重要的一点:这种方法不仅是事件的通知,而且还问您如何处理这种情况的问题。`onFailedToRecycledView()`返回true 表示“无论如何都要回收”。一种合适的情况是,在绑定新项目时 清除所有动画和此问题的其他来源。此时您可以在onFailedToRecycledView()方法中正确处理这些事情
您不应该做的就是onFailedToRecycledView()完全忽略以下是可能会伤害您的一种情况。
想象一下,当您看到项目中的图像时,它们正在褪色。如果用户滚动列表的速度足够快,则当它们消失时,图像将不会完全消失,从而使ViewHolders不符合回收条件。因此,您的滚动会很慢,最重要的是,将创建新的和新的ViewHolders,从而使内存混乱。

成功回收ViewHolder会导致调用onViewRecycled()方法,这是释放大量资源(例如图像)的好地方。请记住,某些ViewHolder实例可能会长时间不使用而坐在池中,这可能会浪费大量内存。现在,我们进入下一个缓存-**View Cache**