详解RecyclerView的预布局

详解,recyclerview,布局 · 浏览次数 : 0

小编点评

**RecyclerView#tryGetViewHolderForPositionByDeadlineViewHolder** ```java ... if (holder == null) { // do not update unless we absolutely have to. holder.mPreLayoutPosition = position; } else { // if bound, update with PreLayoutPosition and position. holder.mPreLayoutPosition = position; ... } ... ``` **分析:** 该方法用于在预布局阶段获取 ViewHolder 的方法。它首先根据 mPreLayoutPosition 值判断是否需要更新 ViewHolder。如果需要更新,它更新 mPreLayoutPosition 值并设置 Holder 的 PreLayoutPosition 属性。然后,它在 holder 的绑定范围内设置位置。 **关键:** * **mPreLayoutPosition** :保存 pre-layout 阶段的 ViewHolder 的 position。 * **mPostLayoutPosition** :保存 post-layout 阶段的 ViewHolder 的 position。 * ** holder.mPreLayoutPosition** :表示 ViewHolder 是否来自预 layout 阶段。 * **holder.mPostLayoutPosition** :表示 ViewHolder 是否来自 post layout 阶段。 **注意:** * 如果 holder 是 null,则不会更新任何东西。 * 如果 mPreLayoutPosition 和 mPostLayoutPosition 等值,则 Holder 的位置已确定。

正文

概述

RecyclerView 的预布局用于 Item 动画中,也叫做预测动画。其用于当 Item 项进行变化时执行的一次布局过程(如添加或删除 Item 项),使 ItemAnimator 体验更加友好。

考虑以下 Item 项删除场景,屏幕内的 RecyclerView 列表包含两个 Item 项:item1 和 item2。当删除 item2 时,item3 从底部平滑出现在 item2 的位置。

+-------+                       +-------+           
|       | <-----+               |       | <-----+   
| item1 |       |               | item1 |       |   
|       |       |               |       |       |   
+-------+     screen   ---->    +-------+     screen
|       |       |               |       |       |   
| item2 |       |               | item3 |       |   
|       | <-----+               |       | <-----+   
+-------+                       +-------+           

上述效果是如何实现的?我们知道 RecyclerView 只会布局屏幕内可见的 Item ,对于屏幕外的 item3,如何知道其要运行的动画轨迹呢?要形成轨迹,至少需要知道起始点,而 item3 的终点位置是很明确的,也就是被删除的 item2 位置。那起点是如何确定的呢?
对于这种情况,Recyclerview 会进行两次布局,第一次被称为 pre-layout,也就是预布局,其会将不可见的 item3 也加载进布局内,得到 [item1, item2, item3] 的布局信息。之后再执行一次 post-layout,得到 [item1, item3] 的布局信息,比对两次 item3 的布局信息,也就确定了 item3 的动画轨迹了。

以下分析过程我们先定义一个大前提:LayoutManager 为 LinearLayoutManager,场景为上述描述的 item 删除场景,屏幕能够同时容纳两个 Item。

预布局

我们知道 RecyclerView 有三个重要的 layout 阶段,分别为:dispatchLayoutStep1、dispatchLayoutStep2 和 dispatchLayoutStep3。这里先直接了当的告知结论:pre-layout 发生于 dispatchLayoutStep1 阶段,而 post-layout 则发生于 dispatchLayoutStep2 阶段。

在执行 item2 的删除时,我们通过调用 Adapter#notifyItemRemoved 来通知 RecyclerView 发生了变化,其调用链如下:

RecyclerView.Adapter#notifyItemRemoved
	RecyclerView.RecyclerViewDataObserver#onItemRangeRemoved
		AdapterHelper#onItemRangeRemoved
		RecyclerView.RecyclerViewDataObserver#triggerUpdateProcessor
			RecyclerView#requestLayout

RecyclerView 中的变更操作会被封装为 UpdateOp 操作,这里删除动作被封装为一个 UpdateOp,添加到 mPendingUpdates 中等待处理。其处理时机为dispatchLayoutStep1 阶段,根据 mPendingUpdates 中的 UpdateOp 来更新列表和 ViewHolder 的信息。

// AdapterHelper#onItemRangeRemoved
boolean onItemRangeRemoved(int positionStart, int itemCount) {
	if (itemCount < 1) {
		return false;
	}
	mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null));
	mExistingUpdateTypes |= UpdateOp.REMOVE;
	return mPendingUpdates.size() == 1;
}

调用链最终会走到 RecyclerView#requestLayout 方法,进而触发 RecyclerView#onLayout 方法的调用。onLayout 做的事比较简单,直接调用 dispatchLayout 将布局事件分发下去,然后将 mFirstLayoutComplete 赋值为true,也就是只要执行过一次 dispatchLayout 那么这个值就会为 true,这个值在后续分析中会用到。

// RecyclerView#onLayout
@Override  
protected void onLayout(boolean changed, int l, int t, int r, int b) {  
    TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);  
    dispatchLayout();  
    TraceCompat.endSection();  
    mFirstLayoutComplete = true;  
}

dispatchLayout 根据状态会调用 layout 的三个重要阶段,保证 layout 三个重要阶段至少被运行一次。

// RecyclerView#dispatchLayout
void dispatchLayout() {

    ...

    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates()
            || needsRemeasureDueToExactSkip
            || mLayout.getWidth() != getWidth()
            || mLayout.getHeight() != getHeight()) {
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else {
        // always make sure we sync them (to ensure mode is exact)
        mLayout.setExactMeasureSpecsFrom(this);
    }
    dispatchLayoutStep3();
}

dispatchLayoutStep1 中根据 mRunPredictiveAnimations 的值来决定是否处于 pre-layout 中,而 mInPreLayout 和 mRunPredictiveAnimations 初始值都是false,因此我们需要找到 mRunPredictiveAnimations 被赋值的地方。

// RecyclerView#dispatchLayoutStep1
private void dispatchLayoutStep1() {

    ...
    mState.mInPreLayout = mState.mRunPredictiveAnimations;
    ...
}

// RecyclerView#State
public static class State {
    boolean mInPreLayout = false;
    
    boolean mRunPredictiveAnimations = false;
}

mRunPredictiveAnimations 由多个变量共同决定,从代码中我们知道只有 mFirstLayoutComplete 为 true 之后才有可能开始运行 ItemAnimator 和预测动画(决定了是否执行 pre-layout),而 mFirstLayoutComplete 只有完成一次 onLayout 才会被赋值为 true,因此可以得知另一个信息:RecyclerView 不支持初始动画。
这里不去一一分析每一个变量的赋值时机,从调试可知这里 mRunSimpleAnimations 和 mRunPredictiveAnimations 会被赋值为true(或者从现象反推,表项删除是带有动画的,因此这里也必须为 true 才能支持表项删除的 ItemAnimator)。

// RecyclerView#processAdapterUpdatesAndSetAnimationFlags
private void processAdapterUpdatesAndSetAnimationFlags() {
    ...
    boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;

    mState.mRunSimpleAnimations = mFirstLayoutComplete
            && mItemAnimator != null
            && (mDataSetHasChangedAfterLayout
            || animationTypeSupported
            || mLayout.mRequestedSimpleAnimations)
            && (!mDataSetHasChangedAfterLayout
            || mAdapter.hasStableIds());
    mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
            && animationTypeSupported
            && !mDataSetHasChangedAfterLayout
            && predictiveItemAnimationsEnabled();
}

继续查看 dispatchLayoutStep1 的其他代码,当可以执行预测动画时,会调用 LayoutManager 的 onLayoutChildren 方法。

private void dispatchLayoutStep1() {
    ...
    processAdapterUpdatesAndSetAnimationFlags();
    
    if (mState.mRunPredictiveAnimations) {
        ...
        mLayout.onLayoutChildren(mRecycler, mState);
    }
    ...
    mState.mLayoutStep = State.STEP_LAYOUT;
}

查看 onLayoutChildren 代码,其注释信息如下:

布局 Adapter 的所有相关子视图。LayoutManager 负责 Item 动画的行为。默认情况下,RecyclerView 有一个非空的 ItemAnimator,并且启用简单的 item 动画。这意味着 Adapter 上的添加/删除操作会伴随有相关动画出现。如果 LayoutManager 的 supportsPredictiveItemAnimations() (默认值)返回 false,并在 onLayoutChildren(RecyclerView.Recycler, RecyclerView.State) 运行正常的布局操作, RecyclerView 也有足够的信息以简单的方式运行这些动画。
当 LayoutManager 想要拥有用户体验更加友好的 ItemAnimator,那么 LayoutManager 应该让 supportsPredictiveItemAnimations() 返回 true 并向 onLayoutChildren( RecyclerView.Recycler、RecyclerView.State) 添加额外逻辑。支持预测动画意味着 onLayoutChildren(RecyclerView.Recycler, RecyclerView.State) 将被调用两次; 一次作为“预”布局来确定 Item 在实际布局之前的位置,并再次进行“实际”布局。在预布局阶段,Item 将记住它们的预布局位置,以便能够被布局正确。 此外,移除的项目将从 scrap 列表中返回,以帮助确定其他项目的正确放置。 这些删除的项目不应添加到子列表中,而应用于帮助计算其他视图的正确位置,包括以前不在屏幕上的视图(称为 APPEARING view),但可以确定其预布局屏幕外位置 给出有关预布局删除视图的额外信息。
第二次布局是真正的布局,其中仅使用未删除的视图。 此过程中唯一的附加要求是,如果 supportsPredictiveItemAnimations() 返回 true,请注意哪些视图在布局之前存在于子列表中,哪些视图在布局之后不存在(称为 DISAPPEARING view),并定位/布局这些视图,而不考虑 RecyclerView 的实际边界。这使得动画系统能够知道将这些消失的视图动画化到的位置。
RecyclerView 的默认 LayoutManager 实现已经处理了所有这些动画要求。 RecyclerView 的客户端可以直接使用这些布局管理器之一,也可以查看它们的 onLayoutChildren() 实现,以了解它们如何解释 APPEARING 和 DISAPPEARING 视图。

简单总结为:为了在 Item 项变更时能够获得更好的动画体验,onLayoutChildren 会被调用两次,一次用于 pre-layout(预布局),一次用于 post-layout(实际布局)。查看代码调用时机,onLayoutchildren 一次在 dispatchLayoutStep1 调用,一次在 dispatchLayoutStep2 调用。

onLayoutChildren 中会调用 fill 函数进行布局填充。能否继续填充由 layoutState.mInfinite 或 remainingSpace 和 layoutState.hasMore(state) 共同决定。
layoutState.mInfinite 表示无限填充,不适用于我们分析的case,因此关键在于 remainingSpace 值的变更。在 pre-layout 中会多布局一次,也就是说相比较于 post-layout,remainingSpace 在 pre-layout 少消费了一次。
remainingSpace 的变更受三个变量控制,由于处在 pre-layout 阶段,因此 state.isPreLayout 为 true,layoutState.mScrapList 此处还未被赋值,因此关注 layoutChunkResult.mIgnoreConsumed 变量。

// LinearLayoutManager#onLayoutChildren
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    fill(recycler, mLayoutState, state, false);
    ...
}

// LinearLayoutManager#fill
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
    ...
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        layoutChunkResult.resetInternal();
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        if (layoutChunkResult.mFinished) {
            break;
        }
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
        if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                || !state.isPreLayout()) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            // we keep a separate remaining space because mAvailable is important for recycling
            remainingSpace -= layoutChunkResult.mConsumed;
        }
        ...
    }
    return start - layoutState.mAvailable;
}

layoutChunkResult 在 fill 过程被当作参数传入 layoutChunk ,因此我们需要关注下是否是这里导致了 layoutChunkResult 的成员变量改变了。
查看 layoutChunk 源码,当 Item 项被标记为 Remove 时,会将 mIgnoreConsumed 变量置为 true,因此在 fill 过程会忽略被删除的 Item 项的布局占用。

// LinearLayoutManager#layoutChunkResult
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
    ...
    if (params.isItemRemoved() || params.isItemChanged()) {
        result.mIgnoreConsumed = true;
    }
}

// RecyclerView#LayoutParams
public boolean isItemRemoved() {  
    return mViewHolder.isRemoved();  
}

// RecyclerView#ViewHolder
boolean isRemoved() {  
    return (mFlags & FLAG_REMOVED) != 0;  
}

ViewHolder 信息更新

我们通过 notifyItemRemoved 来通知 RecyclerView Item 项被删除,那么在什么时候 ViewHolder 被标记为删除状态呢?
在 预布局 一节中我们知道调用最终走到 requestLayout 中,进入触发 onLayout 方法。在布局过程中,关于 ViewHolder 信息更新的调用链路如下:

RecyclerView#dispatchLayoutStep1
	RecyclerView#processAdapterUpdatesAndSetAnimationFlags
		AdapterHelper#preProcess
			AdapterHelper#applyRemove
				AdapterHelper#postponeAndUpdateViewHolders
					AdapterHelper#Callback#offsetPositionsForRemovingLaidOutOrNewView
						RecyclerView#offsetPositionRecordsForRemove
							ViewHolder#offsetPosition
							ViewHolder#flagRemovedAndOffsetPosition

postponeAndUpdateViewHolders 会调用 Adapterhelper#Callback 接口的方法,这个接口在 AdapterHelper 实例化的时候进行实现。最终会走到 offsetPositionRecordsForRemove 方法,这里会对 ViewHolder 相关的 position 和 flag 变量进行修改。

AdapterHelper#Callback 接口实现的地方:

// RecyclerView#initAdapterManager
void initAdapterManager() {  
    mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
        ...
    }
}

回看 dispatchLayoutStep1 阶段的 processAdapterUpdatesAndSetAnimationFlags 调用,在执行 pre-layout 时会调用 mAdapterHelper.preProcess()。

// RecyclerView#processAdapterUpdatesAndSetAnimationFlags
private void processAdapterUpdatesAndSetAnimationFlags() {
    ...
	if (predictiveItemAnimationsEnabled()) {  
	    mAdapterHelper.preProcess();  
	} else {  
	    mAdapterHelper.consumeUpdatesInOnePass();  
	}
	...
}

preProcess 根据 mPendingUpdates 中存储的 UpdateOp 来决定执行相关动作。这与我们上述讲到的 RecyclerView 中的变更操作会被封装为 UpdateOp 操作,添加到 mPendingUpdates 中等待处理 相呼应,这里就是对应的处理逻辑。(额外说一下 AdapterHelper 就是用来存储和处理 UpdateOp 的相关工具类,根据保存的 UpdateOp 列表,计算 position 等信息)

// AdapterHelper#preProcess
void preProcess() {
    mOpReorderer.reorderOps(mPendingUpdates);
    final int count = mPendingUpdates.size();
    for (int i = 0; i < count; i++) {
        UpdateOp op = mPendingUpdates.get(i);
        switch (op.cmd) {
            ...
            case UpdateOp.REMOVE:
                applyRemove(op);
                break;
            ...
        }
        if (mOnItemProcessedCallback != null) {
            mOnItemProcessedCallback.run();
        }
    }
    mPendingUpdates.clear();
}

根据上述的调用链关系,由于中间的调用过程都是一些比较简单的逻辑中转,我们直接查看 RecyclerView#offsetPositionRecordsForRemove 代码的相关逻辑。
由于有 Item 项被删除了,那么它后面的 Item 项的位置信息就需要被更新。这里分两个分支逻辑进行处理:

  • 被删除的 Item 项调用 flagRemovedAndOffsetPosition
  • 被删除的 Item 项之后的 Item 项调用 offsetPosition
void offsetPositionRecordsForRemove(int positionStart, int itemCount,
		boolean applyToPreLayout) {
	final int positionEnd = positionStart + itemCount;
	final int childCount = mChildHelper.getUnfilteredChildCount();
	for (int i = 0; i < childCount; i++) {
		final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
		if (holder != null && !holder.shouldIgnore()) {
			if (holder.mPosition >= positionEnd) {
				holder.offsetPosition(-itemCount, applyToPreLayout);
				mState.mStructureChanged = true;
			} else if (holder.mPosition >= positionStart) {
				holder.flagRemovedAndOffsetPosition(positionStart - 1, -itemCount,
						applyToPreLayout);
				mState.mStructureChanged = true;
			}
		}
	}
	mRecycler.offsetPositionRecordsForRemove(positionStart, itemCount, applyToPreLayout);
	requestLayout();
}

flagRemovedAndOffsetPosition 中将 ViewHolder 的标志位添加上了 FLAG_REMOVED 的删除标志。

// ViewHolder#flagRemovedAndOffsetPosition
void flagRemovedAndOffsetPosition(int mNewPosition, int offset, boolean applyToPreLayout) {
	addFlags(ViewHolder.FLAG_REMOVED);
	offsetPosition(offset, applyToPreLayout);
	mPosition = mNewPosition;
}

此外对于 ViewHolder 的 position 相关信息也会被更新。

void offsetPosition(int offset, boolean applyToPreLayout) {
	if (mOldPosition == NO_POSITION) {
		mOldPosition = mPosition;
	}
	if (mPreLayoutPosition == NO_POSITION) {
		mPreLayoutPosition = mPosition;
	}
	if (applyToPreLayout) {
		mPreLayoutPosition += offset;
	}
	mPosition += offset;
	if (itemView.getLayoutParams() != null) {
		((LayoutParams) itemView.getLayoutParams()).mInsetsDirty = true;
	}
}

预布局和后布局的差异

根据上述的分析我们知道,onLayoutChildren 会被调用两次,一次用于 pre-layout,一次用于 post-layout。那么他们的区别在哪,为何会造成 pre-layout 的布局快照为 [item1, item2, item3], 而 post-layout 的布局快照为 [item1, item3] 呢?

回看 onLayoutChildren 源码,在进行 fill 填充时,会先调用 detachAndScrapAttachedViews 将屏幕内的 Item 项先进行 detach 放置到 mAttachedScrap 中保存。此时 mAttachedScrap 中保存着 item1 和 item2。

// LinearLayoutManager#onLayoutChildren
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    detachAndScrapAttachedViews(recycler);
    ...
    fill(recycler, mLayoutState, state, false);
    ...
}

detachAndScrapAttachedViews 遍历列表中的子 View,将他们都暂时 detach 掉。

// RecyclerView#detachAndScrapAttachedViews
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
	final int childCount = getChildCount();
	for (int i = childCount - 1; i >= 0; i--) {
		final View v = getChildAt(i);
		scrapOrRecycleView(recycler, i, v);
	}
}

// RecyclerView#scrapOrRecycleView
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);
	}
}

在上述分析中,我们知道删除一个 Item 项,其会被添加上 FLAG_REMOVED 的标记位,因此对于 scrapView 的逻辑,其会走入第一个分支逻辑,将 Viewholder 加入到 mAttachedScap 中。

// RecyclerView#Recycler#scrapView
void scrapView(View view) {  
    final ViewHolder holder = getChildViewHolderInt(view);  
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)  
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {  
        if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {  
            throw new IllegalArgumentException("Called scrap view with an invalid view."  
                    + " Invalid views cannot be reused from scrap, they should rebound from"  
                    + " recycler pool." + exceptionLabel());  
        }  
        holder.setScrapContainer(this, false);  
        mAttachedScrap.add(holder);  
    } else {  
        if (mChangedScrap == null) {  
            mChangedScrap = new ArrayList<ViewHolder>();  
        }  
        holder.setScrapContainer(this, true);  
        mChangedScrap.add(holder);  
    }  
}

回到 fill 源码,fill 的核心填充逻辑调用的 layoutChunk。layoutChunk 将获取填充的 View 委托给了 layoutState.next 方法。

// LinearLayoutManager#layoutChunk
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,  
        LayoutState layoutState, LayoutChunkResult result) {  
    View view = layoutState.next(recycler);
    ...
}

LayoutState#next 内部执行逻辑即为 RecyclerView 缓存复用的核心流程。调用链如下:

LayoutState#next
	LinearLayoutManager#Recycler#getViewForPosition
		LinearLayoutManager#Recycler#tryGetViewHolderForPositionByDeadline
			RecyclerView#Recycler#getChangedScrapViewForPosition
			RecyclerView#Recycler#getScrapOrHiddenOrCachedHolderForPosition
			RecyclerView#RecycledViewPool#getRecycledView
			RecyclerView#Adapter#createViewHolder

我们重点关注 getScrapOrHiddenOrCachedHolderForPosition, 因为它包含了从 mAttachedScap 列表获取 ViewHolder 的相关逻辑,而 mAttachedScap 我们上述讲过,onLayoutChildren 过程 detach 的 ViewHolder 会存放在这里。

// RecyclerView#Recycler#getScrapOrHiddenOrCachedHolderForPosition
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;
        }
    }

    ...
    
    return null;
}

对于是否能够复用 mAttachedScap 中的 ViewHolder 取决于多个变量的共同作用。

  • holder.wasReturnedFromScrap(): 由于 ViewHolder 刚被加入到 mAttachedScap 中,因此其还没有被标记上 FLAG_RETURNED_FROM_SCRAP 标志。另外,当能够复用时,被标记了 FLAG_RETURNED_FROM_SCRAP 标志,其也会在 RecyclerView#LayoutManager#addViewInt 中被清除掉。
  • holder.isInvalid():Item 项在删除过程没有被标记上 FLAG_INVALID 标志。
  • mState.mInPreLayout || !holder.isRemoved() 是否处于预布局或不被删除

这里重点讲一下 holder.getLayoutPosition() == position

// RecyclerView#ViewHolder#getLayoutPosition
public final int getLayoutPosition() {
	return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
}

只有当位置不变时,才能被复用。因为从 mAttachedScap 中复用的 ViewHolder 不会再进行 bind 操作。
对于 getLayoutPosition 的取值由 mPreLayoutPosition 和 mPosition 共同作用。这两个变量的取值,其可能会在多处被修改。在初始调用 notifyItemRangeRemoved 通知 RecyclerView 的 Item 项被删除时,在 offsetPositionRecordsForRemove 中 mPosition 会被修正为 Item 已经被删除的正确位置,这个我们在 ViewHolder 信息更新 一节中有过阐述。

item3 由于不存在于 mAttachedScap 和各级缓存中,因此需要被创建。同时在 tryGetViewHolderForPositionByDeadline 中会将 mPosition 和 mPreLayoutPosition 进行正确赋值。
isBound 表示 ViewHolder 是否完成布局,对于刚通过 createViewHolder 创建的 ViewHolder 其为 false, 因此会走入第二个分支逻辑,进行数据绑定以及 position 数据的更新。
注意 offsetPosition 的相关计算,其调用 mAdapterHelper.findPositionOffset(position) 得到。作用与上述讲到的 offsetPositionRecordsForRemove 作用类似。AdapterHelper 会根据 UpdateOp 来为 ViewHolder 提供正确的 position 信息。

// RecyclerView#ViewHolder#tryGetViewHolderForPositionByDeadline
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
    ...
    if (holder == null) {
		...
		holder = mAdapter.createViewHolder(RecyclerView.this, type);
	}
    ...
    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);
    }
    ...
}

此时 pre-layout 的中 ViewHolder 的 position 信息如下:

mPosition mPreLayoutPosition
item1 0 0
item2 0 1
item3 1 2

其得到的布局快照为 [item1, item2, item3]

在 dispatchLayoutStep1 源码中,在函数体结尾处会调用 clearOldPositions 将 mPreLayoutPosition 重置。因此在 dispatchLayoutStep2 中进行 post-layout 过程中调用 getLayoutPosition 的值由 mPosition 决定。

// RecyclerView#dispatchLayoutStep1
private void dispatchLayoutStep1() {
    ...
    if (mState.mRunPredictiveAnimations) {
        mLayout.onLayoutChildren(mRecycler, mState);
        ...
        clearOldPositions();
    }
    ...
}

// RecyclerView#clearOldPositions
void clearOldPositions() {
	final int childCount = mChildHelper.getUnfilteredChildCount();
	for (int i = 0; i < childCount; i++) {
		final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
		if (!holder.shouldIgnore()) {
			holder.clearOldPosition();
		}
	}
	mRecycler.clearOldPositions();
}

// RecyclerView#ViewHolder#clearOldPosition
void clearOldPosition() {  
    mOldPosition = NO_POSITION;  
    mPreLayoutPosition = NO_POSITION;  
}

此时 post-layout 中 ViewHolder 的 position 信息如下:

mPosition mPreLayoutPosition
item1 0 -1
item2 0 -1
item3 1 -1

因此 getScrapOrHiddenOrCachedHolderForPosition 只有 item1 和 item3 能够命中 mAttachedScap 的 ViewHolder,形成 [item1, item3] 的布局快照。

以上分析有很多细节点可能没有很详尽地阐述,因为 RecyclerView 当中应用的概念过于复杂。如果发散开来,形成的篇幅很大,且不易把握主题。
对于 预布局和后布局的差异 这一节中,整体脉络涉及到 Item布局、 ViewHolder 缓存复用以及增删变更带来的 Position 等信息的变化,内容多信息量大,不易快速掌握。建议写一个精简 demo,跟随文章中阐述的调用链实际调试走一把,感受各个变量值变更的过程,加快理解。

与详解RecyclerView的预布局相似的内容:

详解RecyclerView的预布局

概述 RecyclerView 的预布局用于 Item 动画中,也叫做预测动画。其用于当 Item 项进行变化时执行的一次布局过程(如添加或删除 Item 项),使 ItemAnimator 体验更加友好。 考虑以下 Item 项删除场景,屏幕内的 RecyclerView 列表包含两个 Item

详解C#委托与事件

在C#中,委托是一种引用类型的数据类型,允许我们封装方法的引用。通过使用委托,我们可以将方法作为参数传递给其他方法,或者将多个方法组合在一起,从而实现更灵活的编程模式。委托类似于函数指针,但提供了类型安全和垃圾回收等现代语言特性。 基本概念 定义委托 定义委托需要指定它所代表的方法的原型,包括返回类

详解Web应用安全系列(8)不足的日志记录和监控

在Web安全领域,不足的日志记录和监控是一个重要的安全隐患,它可能导致攻击者能够更隐蔽地进行攻击,同时增加了攻击被检测和响应的难度。以下是对Web攻击中不足的日志记录和监控漏洞的详细介绍。 一、日志记录不足的问题 日志缺失或不完整 关键操作未记录:如用户登录、敏感数据访问、系统管理员操作等关键操作未

详解Web应用安全系列(5)敏感数据泄露漏洞

在最近几年,这是最常见的,最具影响力的攻击。这个领域最常见的漏洞是不对敏感数据进行加密。在数据加密过程中,常见的问题是不安全的密钥生成和管理以及使用弱密码算法,弱协议和弱密码。特别是使用弱的哈希算法来保护密码。在服务端,检测数据传输过程中的数据弱点很容易,但检测存储数据的弱点却非常困难。 敏感数据泄

详解Web应用安全系列(4)失效的访问控制

在Web安全中,失效的访问控制(也称为权限控制失效或越权访问)是指用户在不具备相应权限的情况下访问了受限制的资源或执行了不允许的操作。这通常是由于Web应用系统未能建立合理的权限控制机制,或者权限控制机制失效所导致的。 危害 数据泄漏:攻击者可能通过越权访问获取敏感数据,如用户个人信息、财务数据、家

详解Web应用安全系列(3)失效的身份认证

大多数身份和访问管理系统的设计和实现,普遍存在身份认证失效的问题。会话管理是身份验证和访问控制的基础,并且存在于所有有状态的应用程序中。攻击者可以使用指南手册来检测失效的身份认证,但通常会关注密码转储,字典攻击,或者在类似于钓鱼或社会工程攻击之后,发现失效的身份认证。 确认用户的身份,身份验证和会话

详解Web应用安全系列(2)注入漏洞之XSS攻击

上一篇介绍了SQL注入漏洞,今天我们来介绍另一个注入漏洞,即XSS跨站脚本攻击。XSS 全称(Cross Site Scripting) 跨站脚本攻击, 是Web应用中常见的漏洞。指攻击者在网页中嵌入客户端脚本(一般是JavaScript),当用户浏览此网页时,脚本就会在用户的浏览器上执行,从而达到

详解Web应用安全系列(1)注入漏洞之SQL注入

注入漏洞通常是指在可输入参数的地方,通过构造恶意代码,进而威胁应用安全和数据库安全。常见的注入漏洞包括:SQL注入和XSS跨站脚本攻击。 这篇文章我们主要讲SQL注入,SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加

详解Kubernetes Pod优雅退出

1、概述 Pod优雅关闭是指在Kubernetes中,当Pod因为某种原因(如版本更新、资源不足、故障等)需要被终止时,Kubernetes不会立即强制关闭Pod,而是首先尝试以一种“优雅”的方式关闭Pod。这个过程允许Pod中的容器有足够的时间来响应终止信号(默认为SIGTERM),并在终止前完成

详解联邦学习中的异构模型集成与协同训练技术

本文将详细介绍联邦学习中的异构模型集成与协同训练技术,包括基本概念、技术挑战、常见解决方案以及实际应用,结合实例和代码进行讲解。