Android RecyclerView内部机制

1、概述

Android文档中是这么定义RecyclerView的:*A RecyclerView is a flexible view for providing a limited window into a large dataset. *意思是说RecyclerView 是一个能为有限的窗口放入大数据集合的灵活的view。官方的定义说的有些笼统,其实从当时谷歌大会上提到对RecyclerView 的介绍时,其主要目的是为了能取代ListView。使得开发者在开发类似ListView的功能时能更加灵活的实现自己所需要的需求。这边先来说一下Listview的缺点,主要有以下几点:

① 仅能垂直滚动:Android允许Listview仅在垂直方向滚动。但不允许它横向滚动。而且不能将ListView做成网格的形式,只能使用GridView来进行。

② 滞后滚动: ListView的性能非常低下,我们滚动Listview的时候,如果手机的配置不高,会有一种滞后感。这是因为Listview习惯于创建和数据集一样多的itemview。这种创建itemview和使用findViewById()方法时一种开销比较大的操作。虽然有一些优化的方法可以做,但总的来说性能还是不行。

③ 难加入动画:ListView没有内置的动画功能。如果想加入动画会非常麻烦。

而上述ListView的缺点,已经被RecyclerView完美的解决了。RecyclerView是一个新的ViewGroup,用于以水平或垂直或网格或交错网格方式渲染任何基于适配器的视图。

2、RecyclerView相关组件

谷歌实现RecyclerView的方式是将其模块化。RecyclerView如其名字所说用来回收复用View,而其它功能解耦交给其它模块。这种设计思路非常值得学习,各个模块之间分工明确,相互独立又能很好的配合,接下来依次介绍下。

2.1 RecyclerView.Adapter

RecyclerView.Adapter负责从数据集(例如数据库或数组)中提取数据,并将其通过RecyclerView传递给LayoutManager组件,LayoutManager负责将其呈现给用户。

Adapter相关组件结构关系

所以Adapter的作用是将源数据传递给真正使用它的对象。为什么不之间将数据传给LayoutManager,原因是LayoutManager对数据的格式有一定的要求,而源数据中的数据格式并不能满足这个要求,就需要用Adapter对其进行转换成需要的数据*。 *

2.2 RecyclerView.ViewHolder

ViewHolder是RecyclerView和适配器的基石。对于ViewHolder的理解,这里举个十分形象的例子。假设5人去参加派对,派对上有两个摊位摆放小披萨。由于座位有限一次只能招待5名成员。

① 摊位一工作方式:为5个成员中的每一个提供一个盘子,并准备比萨饼,把它放在盘子上。同时额外保留了4个盘子(所以5 + 4 = 9),并且将保留的盘子,两个给接下来马上要排到的人,这样等轮到他们时就不用再给他们分配盘子直接上披萨就好了;2个给已经拿到披萨并离开座位的人,这样当他们想再回来坐着吃的话就可以不用重新给他们盘子和披萨,同时当他们吃完披萨离开就把盘子送到比萨饼店,这样就可以把盘子清理干净,并把它们送给新的2名成员排队等候披萨。所以只有9块盘子的就能够有效地完成工作,并且不会让每个人留下盘子复用。毕竟盘子还挺贵!

② 摊位二工作方式:为每一位顾客准备一个盘子。来一批新人就给他们新的盘子,并在披萨上放上披萨,也不复用盘子,收摊时候直接销毁。

所以第二种工作方式是ListView,第一之后是RecyclerView,而其中所说的盘子就是对应的ItemView,而披萨则是数据,位子代表屏幕可见部分。

Listview与RecyclerView

在RecyclerView中,每个ItemView(盘子)都附带一个ViewHolder。ItemView创建是一项在主线程中的CPU密集任务。减少主线程的CPU任务将有助于提高RecyclerView的性能。通过将ViewHolder附加到视图,能存储View对象的缓存。因此,无论何时我们向上滚动列RecyclerView,最顶端的ViewHolder上的数据都将移动到保持在屏幕上方的ViewHolder中(用户不可见),View保留下来以备重新使用,如果用户向下滚动,ViewHolder已经存储了缓存的数据,因此ViewHolder中的视图和数据可以再次呈现给用户。这个过程使列表高效并提供平滑的滚动。具体ViewHolder的缓存策略之后会详细讲解。

ViewHolder相关组件结构关系

RecyclerView.Adapter的主要工作就是创建和绑定ViewHolder。剩下的东西由ViewHolder处理。

这边要说明一下,我们在Listview中也能使用ViewHolder,并且并没有强制开发者必须使用。但是在RecyclerView中Google强制开发者必须使用,并且对其做了很好的封装和优化。

2.3 RecyclerView.LayoutManager

LayoutManager它的作用前面已经提到,用来从适配器获取视图并在其上安排数据。通过LayoutManager 将数据显示在特定的位置上,并以特定方式将视图定位在屏幕上。如下图所示:

垂直排列,网格排列,瀑布流排列,混合排列

之前使用ListViews,只有垂直列表的选项。而对于其他任何方式,都必须使用其他类似的GroupView。RecyclerView通过为每个布局添加一个LayoutManager来解决这个问题,并且使用它非常方便。

2.4 RecyclerView.ItemAnimator

正如上面所提到的,ListView中的动画处理很不友好。而RecyclerView使用动画就方便许多。并且RecyclerView附带默认动画,并且可以根据需要覆盖和更改默认动画。

RecyclerView动画

上述例子实现在这边:RecyclerView简单的动画

RecyclerView的相关组件就介绍到这,RecyclerView其实还有很多其他组件,例如RecyclerView.ItemDecoration 等等。

3、RecyclerView.Recycler对ViewHolder的查找

前面已经说过RecyclerView会复用ViewHolder接下来想着重阐述一下RecyclerView是如何寻找并复用ViewHolder的。

LayoutManager在铺设item的时候,会去向RecyclerView.Recycler询问,比如说“我要一个position 是8的视图(ViewHolder)”。此时Recycler会按照以下顺序去查找这视图,找到就直接提供,没找到就接着往下寻找:

① 在Changed的 Scrap容器或者Attached的 Scrap容器中查找。

② 在Hidden Views中查找。

③ 在View Cache中查找。

④ 如果Adapter 有Stable Ids,通过id在Attached的 scrap容器和视图缓存中再找一遍。

⑤ 在ViewCacheExtension中查找。

⑥ 在RecycledViewPool 中查找。

如果用上述方法找遍了,还是没有合适的视图,则会调用adapter的onCreateViewHolder()去创建一个视图。然后如果有必要的话,还会调用adapter的onBindViewHolder()方法来绑定视图。之后再返回LayoutManager所需要的视图。对于一个看似简单的LayoutManager请求视图的操作,需要经过如此复杂的步骤,这么做的目的就在于提升RecyclerView的性能,使其能够流畅的运行。上面提到了很多类似于缓存的容器,下面会详细讲解其原理和过程,以及其存在的意义。

3.1 Scrap

Scrap的中文含义是废料,碎屑的意思。在Recycler的搜索顺序中排在最前面。Scrap仅在layout过程中不为空,也就是说在平时,Scrap是这个对象是空对象。当LayoutManager 开始一个layout过程。当前已经在布局中的视图都会转而存储到Scrap中,这一过程在LinearLayoutManager中对应其onLayoutChildren()函数里面调用detachAndScrapAttachedViews()。之后,LayoutManager会通过Recycler逐个索要这些视图,除非这个视图出了一些状况,比如要被删除了等,否则会Recycler会重新从Scrap中取回来给LayoutManager,如下图所示:

视图转储到Scrap再取回

如上图所示,如果有一个删除b视图的操作,就会导致重新布局。这时先将a,b,c放到Scrap中,然后再从Scrap中回收a,c。因为b要被删除,就不再回收回来。至于新补上的d怎么来的后面会讲。虽然b要被删除了,但是b并不会直接抛弃,在布局过程的最后阶段,Recycler发现b没有被添加回布局中去,则RecyclerView会把b隐藏起来(变成Hidden View,下面会讲),并触发b的消失动画,当消失动画结束,会将b放入回收池RecycledViewPool 中。这边可能会有个疑问,对于继续使用的视图例如a和c,这么一个放入取回操作不是多此一举嘛。RecyclerView的设计师对此的解释是:RecyclerView.Recycler和LayoutManager 两者的职责应该分离。LayoutManager 不应该关注某个特定的视图是否是要继续使用,或者对于一个视图是应该从缓存中获取还是是从对象池中获取,这是Recycler应该关注的事情。RecyclerView的设计师的这个设计就很好的遵循了面向对象编程的组件间低耦合,组件内高内聚的原则,非常易于组件内的扩展和组件间的插拔。

在上述Recycler的寻找过程中①涉及到两个Scrap集合,现在来说一下两者的区别。其实在Recycler这个类中,有两个属性mAttachedScrap和mChangedScrap。当一个ViewHolder对应的item被改变(Adapter调用notifyItemChanged() 或者 notifyItemRangeChanged()方法) 并且ItemAnimator 被调用canReuseUpdatedViewHolder()时返回的是false。此时会将这个ViewHolder放到mChangedScrap这个集合中。否则就把ViewHolder放到mAttachedScrap这个集合中。canReuseUpdatedViewHolder()返回false,意味着需要一个视图替换另一个视图的更新动画,例如交叉淡入淡出动画。返回true意味着ItemAnimator 的动画会在一个视图内进行。mAttachedScrap和mChangedScrap集合只有一个区别:mAttachedScrap中的ViewHolder在布局更新前和更新后都会用到,而mChangedScrap中的ViewHolder只有在布局更新前用到,更新完后这个ViewHolder就没用了,因为会有一个新的ViewHolder来替换掉它,更改动画结束后,这个ViewHolder就会放入RecycledViewPool 中。

默认的ItemAnimator在以下种情况下会复用ViewHolder:

① 调用ItemAnimator的setSupportsChangeAnimations(false)方法。

② 调用Adapter的notifyDataSetChanged(),而不是调用notifyItemChanged()或者notifyItemRangeChanged()。

3.2 Hidden Views

Recycler当第一步在Scrap中没有找到想要的ViewHolder,会进入到第二步——去隐藏视图(Hidden Views)中查找。它们是目前正在离开RecyclerView边界的视图,虽然它们目前仍然是RecyclerView的子view,但是为了正确处理它们,从LayoutManager的角度来看,它们已经消失了,也就是说它们不会包含在LayoutManager的计算中。比如说,如果调用了LayoutManager的getChildAt()方法,但这时候一些视图由于对应的item正在被移除,那么这个视图就不应该被计算在内。所有调用getChildAt(), getChildCount(), addView()等这些方法其返回值其实都来自于ChildHelper 这个类,这个类的作用非常简单,就是根据非隐藏子view和所有子view重新计算索引位置。RecyclerView的设计团队设计这个步骤是为了解决下面这种情况出现的bug:

插入item后立刻删除该item

简而言之,就是如果有这么一个操作,用户想在a,b间插入c,同时如果成功插入c的话b就要被顶出屏幕了,但是当c的插入动画还没播放完毕,就立刻来了一个新操作要删除c。对于这么一个骚操作,如果没有Hidden Views,也就是说当接收到插入c通知,并且b没有完全从屏幕上消失的时候就直接把b抛弃,那么当遇到上述情况,Recycler就必须从缓存或者RecycledViewPool 中重新找个新的b对应的ViewHolder,这样就会造成上述情况下,那个被抛弃的ViewHolder在播放消失动画,同时新找来的ViewHolder播放显示动画,这就会造成两个b重叠的bug。引入了Hidden Views,当ViewHolder还没完全消失的时候,万一要重新要拿回来用,就可以很方便的取回。这样即复用了ViewHolder,同时重叠的现象也自然不会发生,动画的过渡就会非常自然。

3.3 View Cache

当ViewHolder的退出动画播放完毕,首先会将ViewHolder扔到mCachedViews中,也就是上面提到的View Cache,这个被称为ViewHolder的一级缓存。这个缓存不区分ViewHolder的类型,默认容量是2,也可以通过RecyclerView的setItemViewCacheSize()来自定义它的大小。当ViewHolder位于View Cache中时,重新取回它不会调用adapter的onBindViewHolder(),也就是说以“原样”的方式重用它,并将其放入对应的位置。

3.4 Stable Ids

如果根据positon还是没有找到想要的ViewHolder,那么按照步骤会如果ViewHolder有稳定的id,会按照id再搜索一遍。这个可以通过重写ViewHolder的getItemId()方法来给ViewHolder设置稳定的id。如果我们调用了adapter的notifyDataSetChanged()方法,但没有设置ViewHolder的id。一般来说最好不要通过notifyDataSetChanged()来通知更新。因为这个方法相当于告诉RecyclerView我对源数据进行了修改,但又不告诉RecyclerView我到底修改了哪,是怎么修改的。这样的话RecyclerView就无法享受到前面Scrap的复用了,因为此时会RecyclerView默认修改了所有的ViewHolder。notifyDataSetChanged()这个方法是为了方便用户从ListView移植到RecyclerView,因为ListView只有这个方法来通知数据已经修改,RecyclerView里其实有更加细粒度的操作。如下图:

notifyDataSetChanged() 但没有Id

而如果我们给ViewHolder设置了id,那么上面那幅图就完全不同了,会如下图:

notifyDataSetChanged() 有Id

Recycler在用position无法找到ViewHolder的时候如果ViewHolder有id就会用id重新在scrap和View Cache中找一遍。

3.5 ViewCacheExtension

这个是开发者设置的自定义缓存。通过调用RecyclerView的setViewCacheExtension((ViewCacheExtension extension))设置,调用View getViewForPositionAndType(Recycler recycler,int position,int type) 获取。除非有特殊需要,不然不建议设置,一个原因是因为可以看到,RecyclerView自带的各种缓存复用已经设计的非常好了,二来ViewHolders不是无状态的,它内部的参数和LayoutManager、RecyclerView有很大的关系,一处理不好就会导致混乱。

3.6 RecycledViewPool

RecycledViewPool是ViewHolder的二级缓存,也是缓存的最后一道防线。同时从RecycledViewPool中获取的ViewHolder会重新调用onBindViewHolder()来重新进行绑定。RecycledViewPool中是按照ViewHolder的type来存储的,每个type类型的ViewHolder有属于自己的容量,默认为5。也可以通过recyclerView.getRecycledViewPool().setMaxRecycledViews(SOME_VIEW_TYPE,POOL_CAPACITY) 来进行设置。比如说如果屏幕中相同类型的ViewHolder很多,就可以把这个类型的容量设置的大一点。如果该类型的ViewHolder只有一个,例如头和尾是特别的ViewHolder,那么就可以把这个容量设置为1。

至此Recycler对ViewHolder的查找过程就结束了,如果在这里Recycler都没有找到想要的ViewHolder,那么只能调用onCreateViewHolder()来创建一个ViewHolder了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容