本篇文章已受权微信公众号 guolin_blog (郭霖)独家发布android
最近在研究 RecyclerView 的回收复用机制,顺便记录一下。咱们知道,RecyclerView 在 layout 子 View 时,都经过回收复用机制来管理。网上关于回收复用机制的分析讲解的文章也有一大堆了,分析得也都很详细,什么四级缓存啊,先去 mChangedScrap 取再去哪里取啊之类的;但其实,我想说的是,RecyclerView 的回收复用机制确实很完善,覆盖到各类场景中,但并非每种场景的回收复用时都会将机制的全部流程走一遍的。举个例子说,在 setLayoutManager、setAdapter、notifyDataSetChanged 或者滑动时等等这些场景都会触发回收复用机制的工做。可是若是只是 RecyclerView 滑动的场景触发的回收复用机制工做时,其实并不须要四级缓存都参与的。api
emmm,应该讲得仍是有点懵,那就继续看下去吧,会一点一点慢慢分析。本篇不会像其余大神的文章同样,把回收复用机制源码一行行分析下来,我也没那个能力,因此我会基于一种特定的场景来分析源码,这样会更容易理解的。废话结束,开始正题。缓存
RecyclerView 的回收复用机制的内部实现都是由 Recycler 内部类实现,下面就都以这样一种页面的滑动场景来说解 RecyclerView 的回收复用机制。微信
相应的版本: RecyclerView: recyclerview-v7-25.1.0.jar LayoutManager: GridLayoutManager extends LinearLayoutManager (recyclerview-v7-25.1.0.jar)源码分析
这个页面每行可显示5个卡位,每一个卡位的 item 布局 type 一致。布局
开始分析回收复用机制以前,先提几个问题:3d
第二个问题以前,先看几张图片:日志
黑框表示屏幕,RecyclerView 先向下滑动,第三行卡位显示出来,再向上滑动,第三行移出屏幕,第一行显示出来。咱们分别在 Adapter 的 onCreateViewHolder() 和 onBindViewHolder() 里打日志,下面是这个过程的日志:cdn
红框1是 RecyclerView 向下滑动操做的日志,第三行5个卡位的显示都是从新建立的 ViewHolder ;红框2是再次向上滑动时的日志,第一行5个卡位的从新显示用的 ViewHolder 都是复用的,由于没有 create viewHolder 的日志,而后只有后面3个卡位从新绑定数据,调用了onBindViewHolder();那么问题来了:xml
在上面的操做基础上,咱们继续往下操做:
在第二个问题操做的基础上,目前已经建立了15个 ViewHolder,此时显示的是第一、2行的卡位,那么继续向下滑动两次,这个过程的日志以下:
红框1是第二个问题操做的日志,在这里截出来只是为了显示接下去的日志是在上面的基础上继续操做的;
红框2就是第一次向下滑时的日志,对比问题2的日志,此次第三行的5个卡位用的 ViewHolder 也都是复用的,并且也只有后面3个卡位触发了 onBindViewHolder() 从新绑定数据;
红框3是第二次向下滑动时的日志,此次第四行的5个卡位,前3个的卡位用的 ViewHolder 是复用的,后面2个卡位的 ViewHolder 则是从新建立的,并且5个卡位都调用了 onBindViewHolder() 从新绑定数据;
那么,
若是明白 RecyclerView 的回收复用机制,那么这三个问题也就都知道缘由了;反过来,若是知道这三个问题的缘由,那么理解 RecyclerView 的回收复用机制也就更简单了;因此,带着问题,在特定的场景下去分析源码的话,应该会比较容易。
其实,根据问题2的日志,咱们就能够回答问题1了。在目前显示一、2行, ViewHolder 的个数为10个的基础上,第三行的5个新卡位要显示出来都须要从新建立 ViewHolder,也就是说,在这个向下滑动的过程,是5个新卡位的复用机制先进行工做,而后第1行的5个被移出屏幕的卡位再进行回收机制工做。
那么,就先来看看复用机制的源码
这个方法是复用机制的入口,也就是 Recycler 开放给外部使用复用机制的api,外部调用这个方法就能够返回想要的 View,而至于这个 View 是复用而来的,仍是从新建立得来的,就都由 Recycler 内部实现,对外隐藏。
因此,Recycler 的复用机制内部实现就在这个方法里。 分析逻辑以前,先看一下 Recycler 的几个结构体,用来缓存 ViewHolder 的。
mAttachedScrap: 用于缓存显示在屏幕上的 item 的 ViewHolder,场景好像是 RecyclerView 在 onLayout 时会先把 children 都移除掉,再从新添加进去,因此这个 List 应该是用在布局过程当中临时存放 children 的,反正在 RecyclerView 滑动过程当中不会在这里面来找复用的 ViewHolder 就是了。
mChangedScrap: 这个没理解是干吗用的,看名字应该跟 ViewHolder 的数据发生变化时有关吧,在 RecyclerView 滑动的过程当中,也没有发现到这里找复用的 ViewHolder,因此这个能够先暂时放一边。
**mCachedViews:**这个就重要得多了,滑动过程当中的回收和复用都是先处理的这个 List,这个集合里存的 ViewHolder 的本来数据信息都在,因此能够直接添加到 RecyclerView 中显示,不须要再次从新 onBindViewHolder()。
mUnmodifiableAttachedScrap: 不清楚干吗用的,暂时跳过。
**mRecyclerPool:**这个也很重要,但存在这里的 ViewHolder 的数据信息会被重置掉,至关于 ViewHolder 是一个重创新建的同样,因此须要从新调用 onBindViewHolder 来绑定数据。
**mViewCacheExtension:**这个是留给咱们本身扩展的,好像也没怎么用,就暂时不分析了。
那么接下去就看看复用的逻辑:
第一步很简单,position 若是在 item 的范围以外的话,那就抛异常吧。继续往下看
若是是在 isPreLayout() 时,那么就去 mChangedScrap 中找。 那么这个 isPreLayout 表示的是什么?,有两个赋值的地方。
emmm,看样子,在 LayoutManager 的 onLayoutChildren 前就会置为 false,不过我仍是不懂这个过程是干吗的,滑动过程当中好像 mState.mInPreLayou = false,因此并不会来这里,先暂时跳过。继续往下。
跟进这个方法看看
首先,去 mAttachedScrap 中寻找 position 一致的 viewHolder,须要匹配一些条件,大体是这个 viewHolder 没有被移除,是有效的之类的条件,知足就返回这个 viewHolder。
因此,这里的关键就是要理解这个 mAttachedScrap 究竟是什么,存的是哪些 ViewHolder。
一次遥控器按键的操做,无论有没有发生滑动,都会致使 RecyclerView 的从新 onLayout,那要 layout 的话,RecyclerView 会先把全部 children 先 remove 掉,而后再从新 add 上去,完成一次 layout 的过程。那么这暂时性的 remove 掉的 viewHolder 要存放在哪呢,就是放在这个 mAttachedScrap 中了,这就是个人理解了。
因此,感受这个 mAttachedScrap 中存放的 viewHolder 跟回收和复用关系不大。
网上一些分析的文章有说,RecyclerView 在复用时会按顺序去 mChangedScrap, mAttachedScrap 等等缓存里找,没有找到再往下去找,从代码上来看是这样没错,但我以为这样表述有问题。由于就咱们这篇文章基于 RecyclerView 的滑动场景来讲,新卡位的复用以及旧卡位的回收机制,其实都不会涉及到mChangedScrap 和 mAttachedScrap,因此我以为仍是基于某种场景来分析相对应的回收复用机制会比较好。就像mChangedScrap 我虽然没理解是干吗用的,但我猜想应该是在当数据发生变化时才会涉及到的复用场景,因此当我分析基于滑动场景时的复用时,即便我对这块不理解,影响也不会很大。
继续往下看
emmm,这段也仍是没看懂,但估计应该须要一些特定的场景下所使用的复用策略吧,看名字,应该跟 hidden 有关?不懂,跳过这段,应该也没事,滑动过程当中的回收复用跟这个应该也关系不大。
这里就要画重点啦,记笔记记笔记,滑动场景中的复用会用到这里的机制。
mCachedViews 的大小默认为2。遍历 mCachedViews,找到 position 一致的 ViewHolder,以前说过,mCachedViews 里存放的 ViewHolder 的数据信息都保存着,因此 mCachedViews 能够理解成,只有原来的卡位能够从新复用这个 ViewHolder,新位置的卡位没法从 mCachedViews 里拿 ViewHolder出来用。
找到 viewholder 后
就算 position 匹配找到了 ViewHolder,还须要判断一下这个 ViewHolder 是否已经被 remove 掉,type 类型一致不一致,以下。
以上是在 mCachedViews 中寻找,没有找到的话,就继续再找一遍,刚才是经过 position 来找,那此次就换成id,而后重复上面的步骤再找一遍,以下
getScrapOrCachedViewForId() 作的事跟 getScrapOrHiddenOrCacheHolderForPosition() 其实差很少,只不过一个是经过 position 来找 ViewHolder,一个是经过 id 来找。而这个 id 并非咱们在 xml 中设置的 android:id, 而是 Adapter 持有的一个属性,默认是不会使用这个属性的,因此这个第5步实际上是不会执行的,除非咱们重写了 Adapter 的 setHasStableIds(),既然不是经常使用的场景,那就先略过吧,那就继续往下。
这个就是常说扩展类了,RecyclerView 提供给咱们自定义实现的扩展类,咱们能够重写 getViewForPositionAndType() 方法来实现本身的复用策略。不过,也没用过,那这部分也看成不会执行,略过。继续往下
这里也是重点了,记笔记记笔记。
这里是去 RecyclerViewPool 里取 ViewHolder,ViewPool 会根据不一样的 item type 建立不一样的 List,每一个 List 默认大小为5个。看一下去 ViewPool 里是怎么找的
以前说过,ViewPool 会根据不一样的 viewType 建立不一样的集合来存放 ViewHolder,那么复用的时候,只要 ViewPool 里相同的 type 有 ViewHolder 缓存的话,就将最后一个拿出来复用,不用像 mCachedViews 须要各类匹配条件,只要有就能够复用。
继续看"图第7步"后面的代码,拿到 ViewHolder 以后,还会再次调用 resetInternal() 来重置 ViewHolder,这样 ViewHolder 就能够看成一个全新的 ViewHolder 来使用了,这也就是为何从这里拿的 ViewHolder 都须要从新 onBindViewHolder() 了。
那若是在 ViewPool 里仍是没有找到呢,继续往下看
若是 ViewPool 中都没有找到 ViewHolder 来使用的话,那就调用 Adapter 的 onCreateViewHolder 来建立一个新的 ViewHolder 使用。
上面一共有不少步骤来找 ViewHolder,无论在哪一个步骤,只要找到 ViewHolder 的话,那下面那些步骤就不用管了,而后都要继续往下判断是否须要从新绑定数据,还有检查布局参数是否合法。以下:
到这里,tryGetViewHolderForPositionByDeadline() 这个方法就结束了。这大概就是 RecyclerView 的复用机制,中间咱们跳过不少地方,由于 RecyclerView 有各类场景能够刷新他的 view,好比从新 setLayoutManager(),从新 setAdapter(),或者 notifyDataSetChanged(),或者滑动等等之类的场景,只要从新layout,就会去回收和复用 ViewHolder,因此这个复用机制须要考虑到各类各样的场景。
把代码一行行的啃透有点吃力,因此我就只借助 RecyclerView 的滑动的这种场景来分析它涉及到的回收和复用机制。
下面就分析一下回收机制
回收机制的入口就有不少了,由于 Recycler 有各类结构体,好比mAttachedScrap,mCachedViews 等等,不一样结构体回收的时机都不同,入口也就多了。
因此,仍是基于 RecyclerView 的滑动场景下,移出屏幕的卡位回收时的入口是:
本篇分析的滑动场景,在 RecyclerView 滑动时,会交由 LinearLayoutManager 的 scrollVerticallyBy() 去处理,而后 LayoutManager 会接着调用 fill() 方法去处理须要复用和回收的卡位,最终会调用上述 recyclerView() 这个方法开始进行回收工做。
回收的逻辑比较简单,由 LayoutManager 来遍历移出屏幕的卡位,而后对每一个卡位进行回收操做,回收时,都是把 ViewHolder 放在 mCachedViews 里面,若是 mCachedViews 满了,那就在 mCachedViews 里拿一个 ViewHolder 扔到 ViewPool 缓存里,而后 mCachedViews 就能够空出位置来放新回收的 ViewHolder 了。
RecyclerView 滑动场景下的回收复用涉及到的结构体两个: mCachedViews 和 RecyclerViewPool
mCachedViews 优先级高于 RecyclerViewPool,回收时,最新的 ViewHolder 都是往 mCachedViews 里放,若是它满了,那就移出一个扔到 ViewPool 里好空出位置来缓存最新的 ViewHolder。
复用时,也是先到 mCachedViews 里找 ViewHolder,但须要各类匹配条件,归纳一下就是只有原来位置的卡位能够复用存在 mCachedViews 里的 ViewHolder,若是 mCachedViews 里没有,那么才去 ViewPool 里找。
在 ViewPool 里的 ViewHolder 都是跟全新的 ViewHolder 同样,只要 type 同样,有找到,就能够拿出来复用,从新绑定下数据便可。
总体的流程图以下:(可放大查看)
最后,解释一下开头的问题
答:先复用再回收,新一行的5个卡位先去目前的 mCachedViews 和 ViewPool 的缓存中寻找复用,没有就从新建立,而后移出屏幕的那行的5个卡位再回收缓存到 mCachedViews 和 ViewPool 里面,因此新一行5个卡位和复用不可能会用到刚移出屏幕的5个卡位。
答:滑动场景下涉及到的回收和复用的结构体是 mCachedViews 和 ViewPool,前者默认大小为2,后者为5。因此,当第三行显示出来后,第一行的5个卡位被回收,回收时先缓存在 mCachedViews,满了再移出旧的到 ViewPool 里,全部5个卡位有2个缓存在 mCachedViews 里,3个缓存在 ViewPool,至因而哪2个缓存在 mCachedViews,这是由 LayoutManager 控制。
上面讲解的例子使用的是 GridLayoutManager,滑动时的回收逻辑则是在父类 LinearLayoutManager 里实现,回收第一行卡位时是从后往前回收,因此最新的两个卡位是0、1,会放在 mCachedViews 里,而二、三、4的卡位则放在 ViewPool 里。
因此,当再次向上滑动时,第一行5个卡位会去两个结构体里找复用,以前说过,mCachedViews 里存放的 ViewHolder 只有本来位置的卡位才能复用,因此0、1两个卡位均可以直接去 mCachedViews 里拿 ViewHolder 复用,并且这里的 ViewHolder 是不用从新绑定数据的,至于二、三、4卡位则去 ViewPool 里找,恰好 ViewPool 里缓存着3个 ViewHolder,因此第一行的5个卡位都是用的复用的,而从 ViewPool 里拿的复用须要从新绑定数据,才会这样只有三个卡位须要从新绑定数据。
答:有时一行只有3个卡位须要从新绑定的缘由跟Q2同样,由于 mCachedView 里正好缓存着当前位置的 ViewHolder,原本就是它的 ViewHolder 固然能够直接拿来用。而至于为何会建立了17个 ViewHolder,那是由于再第四行的卡位要显示出来时,ViewPool 里只有3个缓存,而第四行的卡位又用不了 mCachedViews 里的2个缓存,由于这两个缓存的是0、1卡位的 ViewHolder,因此就须要再从新建立2个 ViewHodler 来给第四行最后的两个卡位使用。