初始化
setAdapter触发初始化,之后开始绘制,依次走以下绘制方法
-
onMeasure
当宽高是 EXACTLY 或 无 adapter 时,直接测量完成,不用再dispatchLayoutStep123等步骤。
如果是其他 AT_MOST 等情况时,就要走step123根据child的宽高决定rv自己宽高了。 -
onLayout
正常情况下我们都是固定组件高度,所以具体处理流程step123会在onLayout部分执行,下面会展开介绍。 -
onDraw 暂时不考虑,这里只针对rv的回收机制研究。
onLayout 流程
- rv.onLayout
- rv.dispatchLayout
- rv.dispatchLayoutStep1、2、3
step1
确定动画等一些准备工作。初始化状态下基本没啥特殊处理。执行完后,状态从 State.STEP_START 到 STEP_LAYOUT
step2
实际处理views布局的方法。
- LinearLayoutManager.onLayoutChildren
- LLM.detachAndScrapAttachedViews
暂时child.size=0,跳过 - LLM.fill
- 先判断 layoutState.mScrollingOffset 是否有值,然后进行recycleByLayoutState回收操作。因为是初始化情况,跳过这步。
- 然后循环LLM.layoutChunk填充剩余空间=整个页面高度
- layoutChunk ->
- layoutState.next ->
- recycler.getViewForPoistion
- rv.tryGetViewHolderForPositionByDeadline
- 初始化时各种缓存都是空的,所以全都会 adapter.createViewHolder + binderViewHolder
- 每次循环layoutChunk后,还会再次判断回收,同上此时不会回收操作。
- LLM.fill
step2会fill两次,以 mAnchorInfo 锚点信息为准,分别朝不同方向尝试填充。对于LinearLayoutManager就是上下方向,初始化的时候锚点是最上面。第一次fill是 fill toawrds end,从最上锚点到最下,将空页面挨个填充满。这里第二次fill,以锚点到最上,没有空间,不做任何填充处理。
执行完后,状态变为STEP_ANIMATIONS
step3
处理动画相关。初始化时无动画,跳过。
滑动情况
初始化后就可以正常使用了,滑动是一个常见的场景
- rv.onTouchEvent MOVE
- rv.scrollByInternal
- rv.scrollStep
- LLM.scrollVerticallyBy
- LLM.scrollBy
- 先 updateLayoutState 确定滚动出来的空间和其他相关数据。
- mLayoutState.mOffset = mOritationHelper.getDecorateEnd(last visible child)
mOffset 代表最后一个可见child最底部,距离rv头部的距离。 - scrollingOffset = oh.getDecrotatedEnd - oh.getEndAfterPadding
后者代表整个rv底部到顶部的距离。所以scrollingOffset代表最后个child被裁减的高度。 - 最后 mLayoutState.mAvailable = 滑动距离deltaY - scrollingOffset
- LLM.fill
- 这次 layoutState.mScrollingOffset 有值,所以进入 recycleByLayoutState 回收操作。
- recycleByLayoutState -> recycleViewFromStart 挨个判断child是否要回收。
- 判断条件 mOrientationHelper.getDecorateEnd/getTransformedEndWithDecoration > limit
其中 limit = scrollingOffset - noRecycleSpace,基本等于滚动的距离。 - 最后,因为是上滑,所以从上到下挨个判断 child的下沿位置,如果超过了 limit 滚动距离,即代表该child是滚动后第一个可见的view。也就代表该child之前的所有view都不可见,可以进行回收了 removeAndRecycleViewAt。
- rv.removeAndRecycleViewAt
- Recycler.recycleView
- Recycler.recycleViewHolderInternal
向mCacheViews中加入vh。如果超过了最大数2,则recycleCachedViewAt 0 移除最早的vh。移除的view将加入RecycledViewPool,每个type默认对多保存5个。
滑动预加载 ( >= 5.0版本后加入的功能)
此外,在 rv.onAttachedToWindow 时会创建一个 GapWorker(runnable),用于预加载数据。rv在滑动的同时也会执行它,会把滑动方向的屏幕外下一个view提前缓存到mCacheViews里。
- gw.run
- gw.prefetch
- gw.flushTasksWithDeadLine
- gw.prefetchPositionWithDeadline
- rv.tryGetViewHolderForPositionByDeadline
第一次尝试获取的话,是空的,且所有缓存都获取不到。然后新建+绑定返回数据。 - 获取到holder后,gw会判断它是否绑定了数据:
- 是的话 Recycler.recycleView 放入mCacheViews。
- 否则 Recycler.addViewHolderToRecycledViewPool 放入缓存池。
- 这里判断进入绑定数据,加入cache缓存。
- 因为prefetch提供默认额外1的数量,所以支持预加载的mCacheViews数量=2+1=3
- rv.tryGetViewHolderForPositionByDeadline
预加载功能可以通过 LayoutManager.setItemPrefetchEnabled false 关闭。还以通过 LayoutManager.setInitialPrefetchItemCount 设置预加载数量,默认spanCount。
adapter.notifyDataSetChanged 刷新情况
- RecyclerViewDataObserver.onChanged
- 先 processDataSetCompletelyChanged 将 vh 都标记为 UPDATE + INVALID。
- 同时将所有 mCachedViews 都移除加到 pool 中。同样进行标记。
- 最后 requestLayout 刷新布局。
- rv.dispatchLayout -> step2 -> LLM.onLayoutChildren
- 先 LM.detachAndScrapAttachedViews -> LM.scrapOrRecycleView
- 这里 vh INVALID + ! isRemoved + ! adapter.hasStableIds 的数据会走回收,否则scrap。
- 后俩条件都满足,且标记了 INVALID, 所以进行 Recycler.recycleViewHolderInternal 回收。
-
回收方法里会判断,被标记 UPDATE INVALID REMOVED 等的都会进pool,不保存cache。
- LLM.fill
同上面流程,这里 tryGet 的数据就会从 pool 里获取了,需要重新绑定。
adapter.notifyItemXXX 局部刷新情况
以 notifyItemChanged 为例
简化流程:
- 先记录刷新未知+数量,然后刷新布局。
- step1先找到非移除的items,进行预布局,保存oldChange信息。
- step2正常布局决定最终要显示的内容。
- 变化的在changed中获取不到,所以新建/从pool获取+绑定;
- 其他的都从attached中获取直接使用。
- step3与step1结果对比vh有没有对应的old信息,有的话进行change动画。
详细流程:
- RecyclerViewDataObserver.onItemRangedChanged
- 先 mAdapterHelper.onItemRangeChanged 记录update范围的信息到 mPendingUpdates 集合里
- 再 RVDO.triggerUpdateProcessor -> requestLayout -> dispatchLayout
核心的step部分如下:
- step1
step1主要是记录原有vh信息,确定要执行哪些动画。notifyItemXX 的都会引起动画相关的,所以这次重点观察下。- processAdapterUpdateAndSetAnimationFlags
- 先 mAdapterHelper.preProcess 获取 mPendingUpdates 里之前保存的 UPDATE 类型信息。
- UPDATE 类型会调用 applyUpdate -> postoneAndUpdateViewHolders -> 再通过 callback 最终回调 rv.markViewHoldersUpdated 方法,该方法会分两步:
- 第一步先 rv.viewRangeUpdate 给 UPDATE 对应的 vh 都加上 FLAG_UPDATE。
- 第二步将 mItemsChanged 设为 true
- mAdapterHelper.preProcess 执行完后,根据 mItemsChanged = true 将动画相关的参数
- mState.mRunSimpleAnimations 和 mState.mRunPredictiveAnimations 也都设为 true
-
mRunSimpleAnimations 判断里会先 mViewInfoStore.addToPreLayout 记录未删除的 vh。然后再 mViewInfoStore.addToOldChangeHolders 记录所有 UPDATE 状态的 vh。
- mRunPredictiveAnimations 判断里会先 saveOldPosition 保存旧的位置信息。然后 onLayoutChildren 以 isLayout 状态预加载一轮 item。
-
先 detachAndScrapAttachedViews -> scrapOrRecycleView 有效数据走 scrap流程。
其中 UPDATE 的保存到 mChangedScrap 里,没更新的保存到 mAttachedScrap 里。 - 再之后会进行 fill 操作,不用于常规情况,这里会有两个区别。
- 首先,isPreLayout 状态会先从 changed 里取 UPDATE 的,其他从 attached 里取。
- 其次,在获取 UPDATE 变化状态的 vh 后,会设置 mIgnoreConsumed 忽略它高度,所以循环方法为了填满空间,还会再补足 vh,会新创建+绑定补 1 个。
- 预加载跑完后 mViewInfoStore.addToAppearedInPreLayoutHolders 记录新创建的vh。
-
- processAdapterUpdateAndSetAnimationFlags
- step2
-
类似常规流程 LM.onLayoutChildren -> fill -> 循环 layoutChunk。而此时是非 isPreLayout 状态,所以无法从 mChangedScrap 里获取 UPDATE 的 vh。因此会新建一条数据,然后绑定使用。其它的画面内 vh 都从 mAttachedScrap 获取直接用。最终1个新的 + 2无修改scrap的就填满空间了,结束循环。结束 fill 方法。
-
onLayoutChildren 在 fill 后还会调用 layoutForPredictiveAnimations 方法,处理剩余部分。此时,剩余部分包括step1时补足控件的1个vh,step2时跳过changed新增的一个vh。这个方法只会获取 mAttachedScrap 的数据,继续调用 fill -> 循环 layoutChunk 流程,不过这次从scrap获取的view不会加到rv上,而是 addDisappearingView 存到某个集合里。
-
- step3
主要是处理动画。-
根据step1 保存的信息,先判断执行更新动画 animateChange。对于非更新等其他动画走 mViewInfoStore.process 执行。
- 动画分为 更新、新增、移除 几种,
- 更新会调用 animateChange
- 新增会回调 processAppeard
- 移除会回调 processDisappeared
- 不变会回调 processPersistent
-
动画通常是多个 ITEM 组合的,比如新增挤走的 item 会 processDisappeared。同理删除 ITEM 可能会让屏幕外新填充的 processAppeard 加进来。新增、不变 都有 preInfo postInfo 俩参数,如果有区别则代表会有位移动画。
-
其中执行 disappeared 动画的 vh 会把消失的 view 加入到 mHiddenViews 中去。
- 动画执行完后会再进行回收 rv.removeAnimationView:
- mChildHelper.removeViewIfHidden 里移除 hidden 里的 view,如果有的话
- 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 呢?
- changed 在有动画情况下,在 step1 预布局时才会使用,先 isPreLayout状态跑一轮,记录信息。
- 然后在step2的时候,变化前 / 待删除 的vh是不用的,所以!isPreLayout状态跳过 changed缓存。
- 最后step3拿step2真正布局的结果和预布局结果对比,进行对应的删除、变化、新增动画。
遗留待研究问题
- mCacheExtension
- 从id获取缓存