RecyclerView 源码解析

Posted by boredream on September 21, 2020

初始化

setAdapter触发初始化,之后开始绘制,依次走以下绘制方法

  • onMeasure
    当宽高是 EXACTLY 或 无 adapter 时,直接测量完成,不用再dispatchLayoutStep123等步骤。
    如果是其他 AT_MOST 等情况时,就要走step123根据child的宽高决定rv自己宽高了。

  • onLayout
    正常情况下我们都是固定组件高度,所以具体处理流程step123会在onLayout部分执行,下面会展开介绍。

  • onDraw 暂时不考虑,这里只针对rv的回收机制研究。

onLayout 流程

  1. rv.onLayout
  2. rv.dispatchLayout
  3. rv.dispatchLayoutStep1、2、3
step1

确定动画等一些准备工作。初始化状态下基本没啥特殊处理。执行完后,状态从 State.STEP_START 到 STEP_LAYOUT

step2

实际处理views布局的方法。

  1. LinearLayoutManager.onLayoutChildren
  2. LLM.detachAndScrapAttachedViews
    暂时child.size=0,跳过
  3. LLM.fill
    1. 先判断 layoutState.mScrollingOffset 是否有值,然后进行recycleByLayoutState回收操作。因为是初始化情况,跳过这步。
    2. 然后循环LLM.layoutChunk填充剩余空间=整个页面高度
      1. layoutChunk ->
      2. layoutState.next ->
      3. recycler.getViewForPoistion
      4. rv.tryGetViewHolderForPositionByDeadline
    3. 初始化时各种缓存都是空的,所以全都会 adapter.createViewHolder + binderViewHolder
    4. 每次循环layoutChunk后,还会再次判断回收,同上此时不会回收操作。
  4. LLM.fill
    step2会fill两次,以 mAnchorInfo 锚点信息为准,分别朝不同方向尝试填充。对于LinearLayoutManager就是上下方向,初始化的时候锚点是最上面。第一次fill是 fill toawrds end,从最上锚点到最下,将空页面挨个填充满。这里第二次fill,以锚点到最上,没有空间,不做任何填充处理。

执行完后,状态变为STEP_ANIMATIONS

step3

处理动画相关。初始化时无动画,跳过。

滑动情况

初始化后就可以正常使用了,滑动是一个常见的场景

  1. rv.onTouchEvent MOVE
  2. rv.scrollByInternal
  3. rv.scrollStep
  4. LLM.scrollVerticallyBy
  5. LLM.scrollBy
    1. 先 updateLayoutState 确定滚动出来的空间和其他相关数据。
    2. mLayoutState.mOffset = mOritationHelper.getDecorateEnd(last visible child)
      mOffset 代表最后一个可见child最底部,距离rv头部的距离。
    3. scrollingOffset = oh.getDecrotatedEnd - oh.getEndAfterPadding
      后者代表整个rv底部到顶部的距离。所以scrollingOffset代表最后个child被裁减的高度。
    4. 最后 mLayoutState.mAvailable = 滑动距离deltaY - scrollingOffset
  6. LLM.fill
    1. 这次 layoutState.mScrollingOffset 有值,所以进入 recycleByLayoutState 回收操作。
    2. recycleByLayoutState -> recycleViewFromStart 挨个判断child是否要回收。
    3. 判断条件 mOrientationHelper.getDecorateEnd/getTransformedEndWithDecoration > limit
      其中 limit = scrollingOffset - noRecycleSpace,基本等于滚动的距离。
    4. 最后,因为是上滑,所以从上到下挨个判断 child的下沿位置,如果超过了 limit 滚动距离,即代表该child是滚动后第一个可见的view。也就代表该child之前的所有view都不可见,可以进行回收了 removeAndRecycleViewAt。
  7. rv.removeAndRecycleViewAt
  8. Recycler.recycleView
  9. Recycler.recycleViewHolderInternal
    向mCacheViews中加入vh。如果超过了最大数2,则recycleCachedViewAt 0 移除最早的vh。移除的view将加入RecycledViewPool,每个type默认对多保存5个。

滑动预加载 ( >= 5.0版本后加入的功能)

此外,在 rv.onAttachedToWindow 时会创建一个 GapWorker(runnable),用于预加载数据。rv在滑动的同时也会执行它,会把滑动方向的屏幕外下一个view提前缓存到mCacheViews里。

  1. gw.run
  2. gw.prefetch
  3. gw.flushTasksWithDeadLine
  4. gw.prefetchPositionWithDeadline
    1. rv.tryGetViewHolderForPositionByDeadline
      第一次尝试获取的话,是空的,且所有缓存都获取不到。然后新建+绑定返回数据。
    2. 获取到holder后,gw会判断它是否绑定了数据:
      1. 是的话 Recycler.recycleView 放入mCacheViews。
      2. 否则 Recycler.addViewHolderToRecycledViewPool 放入缓存池。
    3. 这里判断进入绑定数据,加入cache缓存。
    4. 因为prefetch提供默认额外1的数量,所以支持预加载的mCacheViews数量=2+1=3

预加载功能可以通过 LayoutManager.setItemPrefetchEnabled false 关闭。还以通过 LayoutManager.setInitialPrefetchItemCount 设置预加载数量,默认spanCount。

adapter.notifyDataSetChanged 刷新情况

  1. RecyclerViewDataObserver.onChanged
    1. 先 processDataSetCompletelyChanged 将 vh 都标记为 UPDATE + INVALID。
    2. 同时将所有 mCachedViews 都移除加到 pool 中。同样进行标记。
    3. 最后 requestLayout 刷新布局。
  2. rv.dispatchLayout -> step2 -> LLM.onLayoutChildren
    1. 先 LM.detachAndScrapAttachedViews -> LM.scrapOrRecycleView
    2. 这里 vh INVALID + ! isRemoved + ! adapter.hasStableIds 的数据会走回收,否则scrap。
    3. 后俩条件都满足,且标记了 INVALID, 所以进行 Recycler.recycleViewHolderInternal 回收。
    4. 回收方法里会判断,被标记 UPDATE INVALID REMOVED 等的都会进pool,不保存cache。
  3. LLM.fill
    同上面流程,这里 tryGet 的数据就会从 pool 里获取了,需要重新绑定。

adapter.notifyItemXXX 局部刷新情况

以 notifyItemChanged 为例

简化流程:

  1. 先记录刷新未知+数量,然后刷新布局。
  2. step1先找到非移除的items,进行预布局,保存oldChange信息。
  3. step2正常布局决定最终要显示的内容。
    1. 变化的在changed中获取不到,所以新建/从pool获取+绑定;
    2. 其他的都从attached中获取直接使用。
  4. step3与step1结果对比vh有没有对应的old信息,有的话进行change动画。

详细流程:

  1. RecyclerViewDataObserver.onItemRangedChanged
    1. 先 mAdapterHelper.onItemRangeChanged 记录update范围的信息到 mPendingUpdates 集合里
    2. 再 RVDO.triggerUpdateProcessor -> requestLayout -> dispatchLayout

核心的step部分如下:

  1. step1
    step1主要是记录原有vh信息,确定要执行哪些动画。notifyItemXX 的都会引起动画相关的,所以这次重点观察下。
    1. processAdapterUpdateAndSetAnimationFlags
      1. 先 mAdapterHelper.preProcess 获取 mPendingUpdates 里之前保存的 UPDATE 类型信息。
      2. UPDATE 类型会调用 applyUpdate -> postoneAndUpdateViewHolders -> 再通过 callback 最终回调 rv.markViewHoldersUpdated 方法,该方法会分两步:
        1. 第一步先 rv.viewRangeUpdate 给 UPDATE 对应的 vh 都加上 FLAG_UPDATE。
        2. 第二步将 mItemsChanged 设为 true
      3. mAdapterHelper.preProcess 执行完后,根据 mItemsChanged = true 将动画相关的参数
      4. mState.mRunSimpleAnimations 和 mState.mRunPredictiveAnimations 也都设为 true
    2. mRunSimpleAnimations 判断里会先 mViewInfoStore.addToPreLayout 记录未删除的 vh。然后再 mViewInfoStore.addToOldChangeHolders 记录所有 UPDATE 状态的 vh。

    3. mRunPredictiveAnimations 判断里会先 saveOldPosition 保存旧的位置信息。然后 onLayoutChildren 以 isLayout 状态预加载一轮 item。
      1. 先 detachAndScrapAttachedViews -> scrapOrRecycleView 有效数据走 scrap流程。
        其中 UPDATE 的保存到 mChangedScrap 里,没更新的保存到 mAttachedScrap 里。

      2. 再之后会进行 fill 操作,不用于常规情况,这里会有两个区别。
        1. 首先,isPreLayout 状态会先从 changed 里取 UPDATE 的,其他从 attached 里取。
        2. 其次,在获取 UPDATE 变化状态的 vh 后,会设置 mIgnoreConsumed 忽略它高度,所以循环方法为了填满空间,还会再补足 vh,会新创建+绑定补 1 个。
      3. 预加载跑完后 mViewInfoStore.addToAppearedInPreLayoutHolders 记录新创建的vh。
  2. step2
    1. 类似常规流程 LM.onLayoutChildren -> fill -> 循环 layoutChunk。而此时是非 isPreLayout 状态,所以无法从 mChangedScrap 里获取 UPDATE 的 vh。因此会新建一条数据,然后绑定使用。其它的画面内 vh 都从 mAttachedScrap 获取直接用。最终1个新的 + 2无修改scrap的就填满空间了,结束循环。结束 fill 方法。

    2. onLayoutChildren 在 fill 后还会调用 layoutForPredictiveAnimations 方法,处理剩余部分。此时,剩余部分包括step1时补足控件的1个vh,step2时跳过changed新增的一个vh。这个方法只会获取 mAttachedScrap 的数据,继续调用 fill -> 循环 layoutChunk 流程,不过这次从scrap获取的view不会加到rv上,而是 addDisappearingView 存到某个集合里。

  3. step3
    主要是处理动画。
    1. 根据step1 保存的信息,先判断执行更新动画 animateChange。对于非更新等其他动画走 mViewInfoStore.process 执行。

    2. 动画分为 更新、新增、移除 几种,
      • 更新会调用 animateChange
      • 新增会回调 processAppeard
      • 移除会回调 processDisappeared
      • 不变会回调 processPersistent
    3. 动画通常是多个 ITEM 组合的,比如新增挤走的 item 会 processDisappeared。同理删除 ITEM 可能会让屏幕外新填充的 processAppeard 加进来。新增、不变 都有 preInfo postInfo 俩参数,如果有区别则代表会有位移动画。

    4. 其中执行 disappeared 动画的 vh 会把消失的 view 加入到 mHiddenViews 中去。

    5. 动画执行完后会再进行回收 rv.removeAnimationView:
      1. mChildHelper.removeViewIfHidden 里移除 hidden 里的 view,如果有的话
      2. Recycler.recycleViewHolderInternal 将 changed 的数据加入 cache/pool 缓存。

总结

step1 = pre-layout

  • 决定将要执行什么动画;
  • 保存变化前views信息;

step2 = layout

  • 真正layout排版的方法

step3 = post-layout

  • 保存动画相关信息,并触发动画,然后做一些清理工作

所以综上

正常加载时

只有step2有比较多的操作。step主要方法是LM.onLayoutChildren

notifyDataSetChange时

基本也只有step2有操作,且会利用已有的缓存显示数据。大概流程先 detachAndScrapAttachedViews 回收 vh,这里是统一标记 INVALID + UPDATE,如此标记后的 vh 都会保存到 pool 中。然后再循环 fill 从 pool 中获取并重新绑定数据。

notifyItemXX时

有动画的存在最为复杂。step1时先进行一次pre-layout,LM.onLayoutChildren 预加载一次记录动画前items的情况。同样先 detachAndScrapAttachedViews,将更新的加到 changed 里,其他的加到 attached 里。之后尝试循环fill加回去,因为是预加载状态,所以可以从 changed 里获取数据,其次 attached。但获取的vh如果是删除或改变的状态,会忽略它高度,额外继续取足够的vh补足rv剩余空间。

问题

为什么要忽略高度呢?

因为如果是删除的状态,在动画结束后,需要有个新的item补进来,所以提前算好。

scrap做啥用的?

scrap 存在是因为 LM 和 RV 的分开管理的,所以需要这么个临时的地方 scrap 存放 vh。

为什么要区分 attached 和 changed 两种 scrap 呢?

  1. changed 在有动画情况下,在 step1 预布局时才会使用,先 isPreLayout状态跑一轮,记录信息。
  2. 然后在step2的时候,变化前 / 待删除 的vh是不用的,所以!isPreLayout状态跳过 changed缓存。
  3. 最后step3拿step2真正布局的结果和预布局结果对比,进行对应的删除、变化、新增动画。

遗留待研究问题

  • mCacheExtension
  • 从id获取缓存