前置条件
你已经理解了View绘制的主要流程。下面主要讲一个RecyclerView的设计之美
什么是RecyclerView
A flexible view for providing a limited window into a large data set
怎么翻译:直译的话有点拗口:我把它翻译成 一个在有限的窗口中表示大量数据集灵活的View 对它就是一个View一个不普通的View。
onMeasure
如果LayoutManger是null ,defaultOnMeasure很简单,可以自行去看,但要知道的是界面上不会显示任何子View因为return了
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
自动测量是否开启,RecyclerView自带的几个LayoutManger 重写了 isAutoMeasureEnabled返回的都是true
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
/**
* This specific call should be considered deprecated and replaced with
* {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could
* break existing third party code but all documentation directs developers to not
* override {@link LayoutManager#onMeasure(int, int)} when
* {@link LayoutManager#isAutoMeasureEnabled()} returns true.
*/
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
// Calculate and track whether we should skip measurement here because the MeasureSpec
// modes in both dimensions are EXACTLY.
mLastAutoMeasureSkippedDueToExact =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (mLastAutoMeasureSkippedDueToExact || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
// if RecyclerView has non-exact width and height and if there is at least one child
// which also has non-exact width & height, we have to re-measure.
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
mLastAutoMeasureNonExactMeasuredWidth = getMeasuredWidth();
mLastAutoMeasureNonExactMeasuredHeight = getMeasuredHeight();
}
和layout=null的情况一样调用的是 defaultOnMeasure(widthSpec, heightSpec)没什么讲的
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
设计之美1,有关性能,如果RecyclerView的测量mode是 EXACTLY,其它高度已经定了,直接return,下面的step这些逻辑不用作用了,要知道step1在第一个layout完成后,处理处理adapter设计更改标记时会preLayout(比较耗时的)
mLastAutoMeasureSkippedDueToExact =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (mLastAutoMeasureSkippedDueToExact || mAdapter == null) {
return;
}
如果走到了下面的代码 dispatchLayoutStep1()被称为预布局
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
dispatchLayoutStep1
捕获所有子视图的“旧状态”也就是 “预测位置”为 ItemAnimator(元素动画器)提供数据源供step3使用
省略了部分代码
private void dispatchLayoutStep1() {
processAdapterUpdatesAndSetAnimationFlags();
if (mState.mRunSimpleAnimations) {
// Step 0: Find out where all non-removed items are, pre-layout
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
continue;
}
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
mViewInfoStore.addToPreLayout(holder, animationInfo);
if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
&& !holder.shouldIgnore() && !holder.isInvalid()) {
long key = getChangedHolderKey(holder);
// This is NOT the only place where a ViewHolder is added to old change holders
// list. There is another case where:
// * A VH is currently hidden but not deleted
// * The hidden item is changed in the adapter
// * Layout manager decides to layout the item in the pre-Layout pass (step1)
// When this case is detected, RV will un-hide that view and add to the old
// change holders list.
mViewInfoStore.addToOldChangeHolders(key, holder);
}
}
}
if (mState.mRunPredictiveAnimations) {
// Step 1: run prelayout: This will use the old positions of items. The layout manager
// is expected to layout everything, even removed items (though not to add removed
// items back to the container). This gives the pre-layout position of APPEARING views
// which come into existence as part of the real layout.
// Save old positions so that LayoutManager can run its mapping logic.
saveOldPositions();
final boolean didStructureChange = mState.mStructureChanged;
mState.mStructureChanged = false;
// temporarily disable flag because we are asking for previous layout
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = didStructureChange;
for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
final View child = mChildHelper.getChildAt(i);
final ViewHolder viewHolder = getChildViewHolderInt(child);
if (viewHolder.shouldIgnore()) {
continue;
}
if (!mViewInfoStore.isInPreLayout(viewHolder)) {
int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
boolean wasHidden = viewHolder
.hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (!wasHidden) {
flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
}
final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
if (wasHidden) {
recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
} else {
mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
}
}
}
// we don't process disappearing list because they may re-appear in post layout pass.
clearOldPositions();
} else {
clearOldPositions();
}
stopInterceptRequestLayout(false);
mState.mLayoutStep = State.STEP_LAYOUT;
}
看 processAdapterUpdatesAndSetAnimationFlags
作用是:处理Adapter更新并设置动画标记,给 mRunSimpleAnimations,mRunPredictiveAnimations(运行预测动画)赋值,这两个值都和 mFirstLayoutComplete有关系,但 mFirstLayoutComplete是在第一次layout后才赋值有true的,所以首次Layout时这两个变量全部为false。首次时Adapter更新个毛啊,初使数据有啥更新的啊。
if (mState.mRunSimpleAnimations) 遍历当前屏幕上所有存在的子视图,记录它们在当前布局下的位置、边界、透明度等信息(即“动画开始前的状态”),并存入 ViewInfoStore, 特别处理 isUpdated()(内容更新) 的 ViewHolder:通过 addToOldChangeHolders 将其与一个唯一 Key(通常是 StableId 或 Position)绑定。这样在后续布局中,即使 View 的内容变了(新旧 View 实例不同),RecyclerView 也能通过 Key 将旧 View 与新 View 配对,从而实现内容变化的交叉淡入淡出动画(旧内容淡出,新内容淡入),一句话总结:给所有“即将消失或移动”的旧视图拍一张“遗照”,作为动画的起始帧
if (mState.mRunPredictiveAnimations) 核心作用:在正式布局前额外执行一次预测性布局(调用 mLayout.onLayoutChildren)。它会让 LayoutManager 基于更新后的 Adapter 数据重新摆放所有子项,但保留已移除项的占位空间。
dispatchLayoutStep1 作用:执行预布局,以时间(机器 大概几十ms)换取 人类感知时间,使用用户感官上页面是流畅的,不会那么突兀,保证动画的视觉连续性
为什么要“预布局”?
1.一个经典的删除场景
假设你有一个竖排列表:[A, B, C, D],现在你删除了 B。
没有预布局(直接更新):Adapter 数据变了,RecyclerView 直接布局新数据 [A, C, D]。C 会瞬间从 B 的位置跳到上面去。由于没有“起点”坐标,动画器不知道 C 是从哪里飞过来的,只能让 B 直接消失,C 原地出现——产生“闪烁”和“跳跃”。
有预布局(Step1):在数据正式更新之前,RecyclerView 先把旧数据 [A, B, C, D] 按原样完整布局一次,并拍下“快照”(记录 A、B、C、D 的精确坐标)。此时,系统知道 “C 的起点在 B 的下面”。
然后进入 Step2(正式布局),数据变成 [A, C, D],C 移动到了 B 原来的位置。系统拍下“第二张快照”。
最后在 Step3 中,ItemAnimator 拿到了 C 的 “起点坐标”和 “终点坐标”,它只需要在这两个坐标之间做插值动画(比如平移动画),C 就会平滑地向上滑动,填补 B 的空缺。
结论:没有预布局,动画器就是“瞎子”,不知道元素从哪来;有了预布局,动画器拥有了“前后对照表”,才能做出流畅的过渡。
- 更精妙的一点:预测动画(Predictive Animations)
你注意到源码里的标志位叫 mRunPredictiveAnimations(预测动画)吗?为什么叫“预测”?
因为在 Step1(预布局)执行时,Adapter 的数据其实还没有真正更新。RecyclerView 需要“预测”数据更新后屏幕会变成什么样,从而提前告诉那些屏幕上不存在的、但即将出现的 Item(比如删除 B 后,屏幕底部新露出的 Item E)应该从哪里开始出现。
这种“预判”机制,保证了不仅现有元素移动流畅,新滑入屏幕的元素也能以优雅的动画出现(比如从下方渐入),而不是突兀地蹦出来。 - 首次布局
首次布局没有动画,所以不需要“前后对照”,因此 mRunPredictiveAnimations 为 false,Step1 直接跳过,省下了巨大的性能开销。
非自动测量
方法 setHasFixedSize 性能优化 如果能提前知道RecyclerView的大小不受适配器内容的影响,RecyclerView依然可以更改其size基于其它因素(例如它父View的size)但这种规模的计算不能依赖于 ,其子元素的大小或其适配器的内容(除了适配器中的项数)
如果适配器的更改不会影响RecyclerView的大小那就把它设置为true
于 RecyclerView 尺寸固定(因为fixedSize设置为了true)且已附着窗口,子视图的增删不会影响自身布局尺寸,因此无需立即触发完整的 requestLayout() 重新测量/布局。只需在下一帧绘制前(使用view.post),通过 Runnable 更新子视图的附着/分离状态(例如调用 dispatchLayout() 或 recycleChildren),这样可以 避免多次同步布局请求,将多次更新合并到同一帧,提升滚动或动画过程中的流畅度
void triggerUpdateProcessor() {
if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
} else {
mAdapterUpdateDuringMeasure = true;
requestLayout();
}
}
在测量阶段安全、及时地消费适配器更新,并用正确的标记驱动后续的布局和动画流程
if (mAdapterUpdateDuringMeasure) {
}
如果 mAdapterUpdateDuringMeasure是false并且mRunPredictiveAnimations是true 意味着onMeasure已经执行处理了adapter的所有更改,那说明已经进行了一次测量,如果RV是Linearlayout的子View并且 layout_width=match_parent,Rv不能调用 LM(layoutManager).onMeasure第二次,因为 getViewForPosition 会Crash 当LM使用child 去测量时。
为什么直接return,是因为预布局状态下一些ViewHolder可能是已经被移除的,正要进行真正的布局,这个时候获取的一ViewHolder是不稳当,不可靠的,有可能导致崩溃,所以此时还是直接拿之前测量的宽和高来设置RV的宽和高.
自定义 LayoutManager 的重写:开发者继承 LayoutManager 并重写 onMeasure() 时,完全有可能为了测量 WRAP_CONTENT 或特定尺寸,而去调用 getViewForPosition() 来临时创建一个子 View 计算大小。
如果在预布局状态(mInPreLayout = true)下,第二次 onMeasure 调用了这些含有 getViewForPosition 的逻辑,就会崩溃。 原因有两点:
位置索引错乱:预布局状态下,Adapter 数据已更新,但 getItemCount() 返回的可能是旧数量。getViewForPosition(position) 如果拿到的是新数据的 position,可能会超出旧缓存的范围。
ViewHolder 状态异常:预布局阶段,部分 ViewHolder 被标记为“正在被移除”或“预布局专用”(FLAG_UPDATE 或 FLAG_INVALID),此时试图测量它们,状态是脏的,容易触发断言或空指针
else if (mState.mRunPredictiveAnimations) {
setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
return;
}
RV和其它ViewGroup核心区别是:一般ViewGroup如果到了绘制流程时View树是已经准备好了的,但RV不同,它把添加子View的动作后延到了onMeasure(warp_content AT_MOST)或onLayout(match_parent EXACTLY)阶段因为二者都有可能调用到LM的 onLayoutChildren ,androidx.recyclerview.widget.LinearLayoutManager#fill
在这个方法里有个比较有意思的地方就是 while循环

重点是性能有关的问题,在非Exactly模式下 layoutstate.mInfinite为true,那么如果layoutState.hasMore(state)一直为ture,则这个循环会一直循环下去,
**androidx.recyclerview.widget.LinearLayoutManager.LayoutState#hasMore **
无论是AT_MOST和Exactly 都会走,hasMore的逻辑, 什么说非Exactly浪费性能? 这里就是答案,因为hasMore里的itemCount就是adapter.getItemCount()

androidx.recyclerview.widget.RecyclerView.State#getItemCount

androidx.recyclerview.widget.RecyclerView.Recycler#tryGetViewHolderForPositionByDeadline
对于Scrap 、cache、pool 缓存取出的ViewHolder只要命中else if任何一个条件,都会重新bind,不要以为从Scrap中取得的就一定不会bind的这观念
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"
+ " come here only in pre-layout. Holder: " + holder
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
怎么理解dryRun这个参数?
翻译: Does a dry run, finds the ViewHolder but does not remove 做个预演找到VH但不删除它。
怎么理解:如果是true的话 则只看看,不拿走(不占用、不移出缓存)。如果为false的则是要复用了
/**
* Returns a view for the position either from attach scrap, hidden children, or cache.
*
* @param position Item position
* @param dryRun Does a dry run, finds the ViewHolder but does not remove
* @return a ViewHolder that can be re-used for this position.
*/
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// Try first for an exact, non-invalid match from scrap.
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
//非预演进入判断
if (!dryRun) {
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
// This View is good to be used. We just need to unhide, detach and move to the
// scrap list.
final ViewHolder vh = getChildViewHolderInt(view);
mChildHelper.unhide(view);
int layoutIndex = mChildHelper.indexOfChild(view);
if (layoutIndex == RecyclerView.NO_POSITION) {
throw new IllegalStateException("layout index should not be -1 after "
+ "unhiding a view:" + vh + exceptionLabel());
}
mChildHelper.detachViewFromParent(layoutIndex);
scrapView(view);
vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
| ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
return vh;
}
}
// Search in our first-level recycled view cache.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
// invalid view holders may be in cache if adapter has stable ids as they can be
// retrieved via getScrapOrCachedViewForId
if (!holder.isInvalid() && holder.getLayoutPosition() == position
&& !holder.isAttachedToTransitionOverlay()) {
if (!dryRun) {
//如果非预演则移除,因为上面获取的ViewHolder要供上层使用
mCachedViews.remove(i);
}
if (DEBUG) {
Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
+ ") found match in cache: " + holder);
}
return holder;
}
}
return null;
}
| 类型 | 是否内存缓存 | 本质 |
|---|---|---|
| scrap | ✔ 是 | “正在布局过程中暂存的 ViewHolder” |
| hidden children | ✔ 是 | “仍在 View 层级,但被隐藏的 View” |
| mCachedViews | ✔ 是 | “可直接复用的 ViewHolder 缓存池” |
为什么 dispatchLayout 、onLayoutChildren时要先 detachAndScrapAttachedViews
因为 LayoutManager 在开始布局前并不知道哪些 ViewHolder 最终还需要,所以先把当前所有子 View detach 并放入 scrap,再由 fill() 按新的布局结果决定哪些重新 attach、哪些最终回收。
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
final int childCount = getChildCount(); //这个count是屏幕高或高size/itemSize宽或高
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
scrapOrRecycleView废弃或者回收视图
1、存入RecyclerPool中
条件:viewHolder.isInvalid(),说明是无效的,意味着不能绑定旧数据
条件:!viewHolder.isRemoved() 说是不是 notifyItemRemoved()产生的
条件:!adapter.hasStableIds() 如果有 StableId:RecyclerView 即使数据刷新,也能根据 id 找回旧 Holder
总结是 如果这个 Holder 已失效,而且没有 StableId 可以重新匹配,就不要 Scrap,直接回收
remove: View直接从ChildHelper删除
detach: View还存在Recycler中
2、进入 Scrap (供本次布局快速复用)
deatch后这个View不在View树中了,但View对象还存在,从布局树中脱离
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
if (DEBUG) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
再巩固一下获取的流程

RecyclerView 1.2.1版本缓存分层
Layout开始
│
▼
① Attached Scrap (layout期间)
│
▼
② Changed Scrap(PreLayout时才有 动画 )
│
▼
③ mCachedViews
│
▼
④ ViewCacheExtension(开发者扩展)
│
▼
⑤ RecycledViewPool
│
▼
⑥ Adapter.createViewHolder()
mAttachedScrap:在layout时 scrapOrRecycleView方法中放进 mAttachedScrap,当前屏幕上的马上重新布局的
Changed Scrap:notifyItemChanged 这种 PreLayout 动画期间才存在 保存 旧ViewHolder 用于 ItemAnimator change动画
mCachedViews:View Cache 保存刚刚滑出屏幕的 size的大小是 2 ,已经bind好的VH,不用重新bind 例如 position = 2 向上滑动,再向下滑,可以直接拿出,不需要重新bind,也是在 scrapOrRecycleView中的逻辑。
RecycledViewPool:真正意义上的 回收池里面已经没有Position 只有: Text、Image、Banner、都是分开的池 (准确说已经回收,不再对应Position)如果:viewType一样 直接拿 然后bind 记着一定要bind
RecyclerView回收流程


如果只是scrap(只是和RV解除父子关系)或其还挂载在RV上不能回收
if (holder.isScrap() || holder.itemView.getParent() != null) {
throw ...
}
不归Recycler管理的VH ,shouldIgnore() 为 true 的 ViewHolder,不会进入 Recycler 的 Scrap、mCachedViews、RecycledViewPool 等缓存流程,它仍然属于 RecyclerView,只是暂时不参与 Recycler 的缓存与回收管理
if (holder.shouldIgnore()) {
throw new IllegalArgumentException("Trying to recycle an ignored view holder. You"
+ " should first call stopIgnoringView(view) before calling recycle."
+ exceptionLabel());
}
前面所有判断都是VH有没有资格进入回收。