RecyclerView 源码分析

640?wx_fmt=png

今日科技快讯

9月2日晚间,华为终端宣布,将于10月16日在德国慕尼黑发布华为 Mate 10 新机。从放出的海报来看,证实了华为 Mate 10 将搭载自家的麒麟970处理器,并采用时下流行的全面屏设计。根据之前的报道,华为 Mate 10依然使用了徕卡品牌的摄像头,同时两个摄像头分别是1200万彩色和2000万黑白组合。另外该机搭载 QHD“Entire View” 显示屏,屏幕尺寸为6英寸。据悉,Mate10 还将集成人工智能功能。

作者简介

经过短暂的调整,小伙伴们新的一周继续加油哦!

本篇来自 MeloDev 的投稿,详细的讲解了 RecyclerView 的源码,希望大家喜欢!

MeloDev 的博客地址:

http://www.jianshu.com/u/f5909165c1e8

写在前面

RecyclerView 是一个越用越优雅的控件,相信大家对于 RecyclerView 的使用也已经比较熟悉了。其功能的高度解耦化,规范 ViewHolder 的写法,以及对动画友好支持,都是它与传统控件 ListView 的区别。而无论 ListView 还是 RecyclerView,本质上都是在有限的屏幕之上,展示大量的内容。所以复用的逻辑,就成了它们最最重要的核心原理,本文主要目的就是探究 RecyclerView 的复用原理。

正文

RecyclerView 的几大模块:

  • LayoutManager
    负责 RecyclerView 中,控制 item 的布局方向

  • RecyclerView.Adapter
    为 RecyclerView 承载数据

  • ItemDecoration
    为 RecyclerView 添加分割线

  • ItemAnimator
    控制 RecyclerView 中 item 的动画

刚刚我们提到 RecyclerView 的高度解耦,就是通过以上对象各司其职,来实现 RecyclerView 的基本功能。RecyclerView 无论多么复杂,本质上也是一个自定义 View,本文的重点就是缓存原理分析,不过在此之前,我们还是简单地分别介绍下以上中各个模块的源码

ItemDecoration

RecyclerView 源码分析_第1张图片

mItemDecorations 是一个 ArrayList,我们将 ItemDecoration 也就是分割线对象,添加到其中。接着我们看下 markItemDecorInsetsDirty 这个方法做了些什么。

RecyclerView 源码分析_第2张图片

这个方法首先遍历了 RecyclerView 和 LayoutManager 的所有子 View,将其子 View 的 LayoutParams 中的 mInsetsDirty 属性置为 true。接着调用了 mRecycler.markItemDecorInsetsDirty(),Recycler 是 RecyclerView 的一个内部类,就是它管理着 RecyclerView 的复用逻辑。这个我们一会再细谈。

RecyclerView 源码分析_第3张图片

mCachedViews 见名知意,也就是 RecyclerView 缓存的集合,相信你也看到了,RecyclerView 的缓存单位是 ViewHolder。我们在 ViewHolder 中取出 itemView,然后获得 LayoutParams,将其 mInsetsDirty 字段一样置为 true。

mInsetsDirty 字段的作用其实是一种优化性能的缓存策略,添加分割线对象时,无论是 RecyclerView 的子 view,还是缓存的 view,都将其置为 true,接着就调用了 requestLayout 方法。

这里简单说一下 requestLayout 方法用一种责任链的方式,层层向上传递,最后传递到 ViewRootImpl,然后重新调用 view 的 measure、layout、draw 方法来展示布局。

我们在 RecyclerView 中搜索 mItemDecorations 集合,看看他是在什么时刻操作 ItemDecoration 这个分割线对象的。

在 onDraw 中:

RecyclerView 源码分析_第4张图片

在 draw 方法中:

RecyclerView 源码分析_第5张图片

可以看到在 View 的以上两个方法中,分别调用了 ItemDecoration 对象的 onDraw onDrawOver 方法。

这两个抽象方法,由我们继承 ItemDecoration 来自己实现,他们区别就是 onDraw 在 item view 绘制之前调用,onDrawOver 在 item view 绘制之后调用。

所以绘制顺序就是 Decoration 的 onDraw,ItemView的 onDraw,Decoration 的 onDrawOver。

(好像越写越多...收不住了...)

我们在 onDraw 和 onDrawOver 方法中就可以绘制 drawable 对象了。此时分割线就展现出来了。还记得刚才的 mInsetsDirty 字段吗?在添加分割线的时候,无论是 RecyclerView 子 View,还是缓存中的 View,其 LayoutParams 中的 mInsetsDirty 属性,都被置为 true。 我们来解释一下这个字段的作用:

RecyclerView 源码分析_第6张图片

来解释一下这段代码,首先 getItemDecorInsetsForChild 方法是在 RecyclerView 进行 measureChild 时调用的。目的就是为了取出 RecyclerView 的 ChildView 中的分割线属性 --- 在 LayoutParams 中缓存的 mDecorInsets 。而 mDecorInsets 就是 Rect 对象, 其保存记录的是所有添加分割线需要的空间累加的总和,由分割线的 getItemOffsets 方法影响。

最后在 measureChild 方法里,将分割线 ItemDecoration 的尺寸加入到 itemView 的 padding 中。

但是大家都知道缓存并不是总是可用的,mInsetsDirty 这个 boolean 字段来记录它的时效性,当 mInsetsDirty 为 false 时,说明缓存可用,直接取出可以,当 mInsetsDirty 为 true 时,说明缓存的分割线属性就需要重新计算了。

到此,关于 RecyclerView 添加分割线 ItemDecoration 的源码分析,也就基本结束了。

ItemAnimator

如果有人问我,在什么情况下你绝对会选择 RecyclerView,而不是 ListView?如果需求对 Item 的动画有一定要求,这绝对是我选择 RecyclerView 的重要原因之一。ListView 如果要做 Item 的增删动画,那可要费很大劲儿,而 RecyclerView 自身对动画就有很好的支持。

RecyclerView 源码分析_第7张图片

这个方法就是为 RecyclerView 设置动画的入口,逻辑就是清除旧的 Listener,设置新的 Listener。这个没什么可多说的,我们这次直接来看看 ItemAnimator 这个类。

当我第一次粗略的看 RecyclerView 的源码之时,感觉这也太多了吧,一万两千多行,这得多复杂啊。后来再看看,才知道,RecyclerView 是把 ItemAnimator 、LayoutManager 等等这些模块都当内部类写在了一块。。。我不知道这样设计的目的是啥,除了写着方便之外可能找不到其他理由了,so... 可能是 google 的程序员偷个懒吧 - -

来看 ItemAnimator 类:

RecyclerView 源码分析_第8张图片

这两个函数看命名也能猜一个大概,其目的就是为了记录在 RecyclerView 布局之前/之后,必要的一些 layout 信息保存在 ItemHolderInfo 中,ItemHolderInfo 这个类就是用来记录当前 ItemView 的位置信息

recordPreLayoutInformation 来记录 layout 之前的状态信息,这个方法在 dispatchLayoutStep1 之中调用。

dispatchLayoutStep1这个方法做了什么呢,来看看注释就一目了然:

RecyclerView 源码分析_第9张图片

这是布局的第一步:

进行 adapter 布局的更新,决定执行哪个动画,保存当前 view 的信息,如果有必要,运行 predictive layout。

相同的 recordPostLayoutInformation 方法来记录 layout 过程完成时,ItemView 的信息

它在 dispatchLayoutStep3 方法中调用,dispatchLayoutStep3 方法作用:

640?wx_fmt=jpeg

layout 的最后一个步骤,保存 view 动画的信息,执行动画,状态清理。当然也有 dispatchLayoutStep2 方法,他们三个方法依次在 onLayout 方法的 dispatchLayout方法调用。

dispatchLayout 方法目的是 layout RecyclerView 的 childview,并且记录动画执行的过程、变更。

现在知道了这两个 API 的作用就是在 layout 前后记录 itemview 动画的状态,保存在 ItemHolderInfo 中,我们继续寻找执行动画的 API。

  • animateDisappearance
    当 ViewHolder 从 RecyclerView 的 layout 中移除时,调用

  • animateAppearance
    当 ViewHolder 添加进 RecyclerView 时,调用

  • animatePersistence
    当 ViewHolder 已经添加进 layout 还未移除时,调用

  • animateChange
    当 ViewHolder 已经添加进 layout 还未移除,并且调用了 notifyDataSetChanged 时,调用。

以上四个 api 就是为 RecyclerView 执行动画的。

调用的时机和方式是如何的呢?

RecyclerView 源码分析_第10张图片

mViewInfoProcessCallback 是一个匿名内部类,在其回调方法中,分别执行了以上四个关于动画的 api。

继续跟进,看看 mViewInfoProcessCallback 这个接口是什么时候被调用执行的:

640?wx_fmt=jpeg

执行触发方法的位置就在我们刚才提到的 dispatchLayoutStep3方法中,去根据一些保存的 flag 状态去触发动画。

LayoutManager

与其他绑定 adapter 展示数据的控件,比如 ListView、GrideView 相比,RecyclerView 允许自定义规则去放置子 view,这个规则的控制者就是 LayoutManager。一个 RecyclerView 如果想展示内容,就必须设置一个 LayoutManager。

我们按照惯例来看设置 LayoutManager 的入口:

RecyclerView 源码分析_第11张图片

这段代码主要做了一下几件事:

当之前设置过 LayoutManager 时,移除之前的视图,并缓存视图在 Recycler 中,将新的 mLayout 对象与 RecyclerView 绑定,更新缓存 View 的数量。最后去调用 requestLayout ,重新请求 measure、layout、draw。

关于 RecyclerView 的缓存我们一会再研究,先看看设置的 LayoutManager 是在何时何处发挥作用的吧:

LayoutManager 作为 RecyclerView 的一个抽象内部类,大概有3000行代码,方法数还很多,看着头都大了。用有限的时间去了解每一个方法的作用显然不现实。LayoutManager 的作用就是为 RecyclerView 放置子 view,所以我直接去定位 RecyclerView 的 onLayout 和 onMeasure 方法,研究一下 LayoutManager 的一些关键函数的作用。

RecyclerView 源码分析_第12张图片

来分析一下 onMeasure 方法,mAutoMeasure 字段用来标记是否使用 RecyclerView 的默认规则进行自动测量,否则就必须在 LayoutManager 中自己实现 onMeasure 来进行测量。LinearLayoutManager 的 mAutoMeasure 字段属性就被设置成为了 true。

所以我们重点来看mAutoMeasure为 true 时,测量的规则。

当 RecyclerView 的 MeasureSpec 为 MeasureSpec.EXACTLY时,这个时候可以直接确定 RecyclerView 的宽高,所以 return 退出测量。当 RecyclerView 的宽高为不为 EXACTLY 时,首先进行的测量步骤就是 dispatchLayoutStep1,这个我们在分析动画源码的时候提到过。dispatchLayoutStep1 的作用总结起来就是记录 layout 之前,view 的信息。

接着继续调用了 dispatchLayoutStep2方法:

RecyclerView 源码分析_第13张图片

dispatchLayoutStep2 比较关键的就是以上代码展示的这两步,onLayoutChildren 这个函数由 LayoutManager 实现,来规定放置子 view 的算法,寻找锚点填充 view,锚点的寻找和填充 view 的方式,这里就不细说了。因为篇幅实在是太长。具体可以直接去看 LinearLayoutManager 的实现方式。

第二步就是将 mState.mLayoutStep 置为 State.STEP_ANIMATIONS,刚才我们忘记说 mLayoutStep 这个属性了,从它的命名就知道它是来标记 layout 这个过程进行到哪一步了。在 dispatchLayoutStep1 中 mState.mLayoutStep 被置为 State.STEP_LAYOUT,记录 layout 的步骤是什么原因呢?来看看 RecyclerView 的 onLayout 方法:

RecyclerView 源码分析_第14张图片

跟进 dispatchLayout

RecyclerView 源码分析_第15张图片

看到 mState.mLayoutStep 的作用了吧,当我们在 onMeasure 方法中已经调用过 dispatchLayoutStep1 、 dispatchLayoutStep2 时,在 onLayout 方法中只会调用 dispatchLayoutStep3,dispatchLayoutStep3方法我们在动画部分的源码讲解过。

回想一下,什么情况下会在 onMeasure 方法中直接调用 dispatchLayoutStep1 、 dispatchLayoutStep2 ? 就是 RecyclerView 的 MeasureSpec 不为 EXACTLY 时,这个情况下 RecyclerView 不能自己确定自身的宽高,只能在测量、布局了子 view 才能确定自己的宽高。所以在 onMeasure 的时候就调用了 dispatchLayoutStep1 、 dispatchLayoutStep2 ,在 onLayout 仅仅调用 dispatchLayoutStep3 方法就可以了。

如果文字表述的不够清晰,这里来一张图:

RecyclerView 源码分析_第16张图片

onMeasure/onLayout

最后在这个章节中,总结一下 LayoutManager 的作用:

  • 协助 RecyclerView 完成 onMeasure 过程

  • 通过 onLayoutChildren 完成对子 view 的布局

  • 滚动子视图

  • 滚动过程中判断何时添加 view ,何时回收 view,也就是对缓存时机的判断。

Recycler

这篇文章我在工作之余写写停停大概花了一周,终于到了本文的最后一个模块 -- recycler

在上文中反复提到,Recycler 就是控制 RecyclerView 缓存的核心类。理解了它的工作过程,就可以弄明白 RecyclerView 如何回收 view,如何复用 view 的。

在上一个章节,我们说过了 LayoutManager 其实就是 Recycler 的控制者,由 LayoutManager 来决定调用 Recycler 关键方法的时机,口说无凭,就直接来看看 LinearLayoutManager 的代码吧:

首先来看最关键的部分 onLayoutChildren 方法,这个方法是在 RecyclerView 的dispatchLayoutStep2 阶段调用的。

onLayoutChildren 方法比较长,它核心的作用就是寻找一个锚点,为 RecyclerView 填充子 View。在 onLayoutChildren 调用的 fill 方法就是真正开始填充 layout 的方法。

RecyclerView 源码分析_第17张图片

这就是省略了部分代码的 fill 方法,其中的 while 循环中,通过判断 LaytouState 中保存的状态来不断的通过 LayoutChunk 方法填充 view。所以接着再看 LayoutChunk 方法。

RecyclerView 源码分析_第18张图片

以上就是 layoutChunk 的部分代码,可以看到通过 next 方法取出来 view ,并且通过 addView 添加到 RecyclerView 里面去,继续跟进next 方法,看看它是用什么方式和规则取出来 View 的。

RecyclerView 源码分析_第19张图片

一路看到这里,我们的 Recycler 终于出现了...跟进 getViewForPosition 方法吧。

在此之前可以先看看 Recycler 几个重要的成员变量,便于我们更好的认识 RecyclerView 的缓存结构:

RecyclerView 源码分析_第20张图片

提前说一下,RecyclerView 其实可以算作是四级缓存、mAttachedScrap 、mCachedViews 、mViewCacheExtension、mRecyclerPool 这四个对象就是作为每一级缓存的结构的。

getViewForPosition 方法:

640?wx_fmt=jpeg

跟进 tryGetViewHolderForPositionByDeadline方法,一段一段的阅读这个方法代码。

在文章之初我们就说过,RecyclerView 的缓存单元是 ViewHolder,这个tryGetViewHolderForPositionByDeadline 方法就完整的展示了如何在每个层级的缓存中,取出来 ViewHolder,下面我们一步步的分析一下:

RecyclerView 源码分析_第21张图片

第一步,从 mChangedScrap 中尝试取出缓存的 ViewHolder ,若不存在,返回空。

RecyclerView 源码分析_第22张图片

如果在第一步发现没有缓存的 ViewHolder,则去 mAttachedScrap 中取,当然 mAttachedScrap 中没有怎么办呢,接着去 mHiddenViews 里面去找,如果还没有,继续从 mCachedViews 中取缓存的 ViewHolder ,这一系列操作是缓存层级的第二步。

RecyclerView 源码分析_第23张图片

RecyclerView 源码分析_第24张图片

在 getScrapOrCachedViewForId 方法中,根据 id 依次在 mAttachedScrap 、mCachedViews 集合中寻找缓存的 ViewHolder,如果都不存在,则在 ViewCacheExtension 对象中寻找缓存,ViewCacheExtension 这个类需要使用者通过 setViewCacheExtension 方法传入,RecyclerView 自身并不会实现它,一般正常的使用也用不到。缓存层级的第三步,我们就分析完毕了,来看最后一步。

RecyclerView 源码分析_第25张图片

这段代码是从 RecycledViewPool 中取根据 type 取 ViewHolder,对于 RecycledViewPool 下面多说几句:

RecyclerView 源码分析_第26张图片

从这里我们知道,RecycledViewPool 其实是一个 SparseArray 保存 ScrapData 对象的结构。根据 type 缓存 ViewHolder,每个 type,默认最多保存5个 ViewHolder。上面提到的 mCachedViews 这个集合默认最大值是 2 。

RecycledViewPool 可以由多个 ReyclerView 共用。

RecycledViewPool 就是缓存结构中的第四级缓存了,如果 RecycledViewPool 中依然没有缓存的 ViewHolder 怎么办呢?

RecyclerView 源码分析_第27张图片

这个时候没有办法,就只能调用 mAdapter.createViewHolder(RecyclerView.this, type),来创建一个 ViewHolder 了。

到此我们就知道一个 ViewHolder 是如何层层从缓存中取出的了。

写在后面

本文从源码的角度研究了 RecyclerView 的主要模块和功能,但是 RecyclerView 本身是很复杂的,要考虑到非常多的情况,光是各种状态的记录就让人看得很迷,这种复杂度的控件真不是一般程序员可以搞定的。不过我们不必深究每一块细节,将大致的流程梳理在心,也肯定会有收获和启发。未来打算总结下 RecyclerView 可能发生的一些复用问题。

更多

每天学习累了,看些搞笑的段子放松一下吧。关注最具娱乐精神的公众号,每天都有好心情。

RecyclerView 源码分析_第28张图片

如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。

欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号:

RecyclerView 源码分析_第29张图片

你可能感兴趣的:(RecyclerView 源码分析)