疑问

1、RecyclerView缓存复用机制原理?
2、pre-layout和prefetch原理?

RecyclerView 核心类图

1、核心流程

核心流程源码:

1、三个layout流程

/**
 * The first step of a layout where we;
 * - process adapter updates
 * - decide which animation should run
 * - save information about current views
 * - If necessary, run predictive layout and save its information
 */
private void dispatchLayoutStep1() {
...
}
/**
 * The second layout step where we do the actual layout of the views for the final state.
 * This step might be run multiple times if necessary (e.g. measure).
 */
private void dispatchLayoutStep2() {
...
}
/**
 * The final step of the layout where we save the information about views for animations,
 * trigger animations and do any necessary cleanup.
 */
private void dispatchLayoutStep3() {
...
}
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    ...
    ViewHolder holder = null;
    // 0) If there is a changed scrap, try to find from there
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    // 1) Find by position from scrap/hidden list/cache
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        ...
    }
    if (holder == null) {
        ...

        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) Find from scrap/cache via stable ids, if exists
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            ...
        }
        if (holder == null && mViewCacheExtension != null) {
            // We are NOT sending the offsetPosition because LayoutManager does not
            // know it.
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
                ...
            }
        }
        if (holder == null) { // fallback to pool
            ...
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                ...
            }
        }
        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()) {
        ...
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
...
    return holder;
}

1.1、屏幕内复用

当屏幕内的内容进行更新时的缓存

1.1.1 ArrayList mChangedScrap

1.1.1.1 缓存存入时机

当执行notifyItemChanged方法(比如动画刷新某个列表项)时,会使holder.needsUpdate()为true,把除了做动画的item放进mChangedScrap,其余的item则放进mAttachedScrap。


void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        ...
        mAttachedScrap.add(holder);
    } else {
        ...
        mChangedScrap.add(holder);
    }
}

1.1.1.2 缓存取出时机,只有在mState.isPreLayout()状态下会从ChangedScrap中取缓存,dispatchLayoutStep1的时候会执行

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
                ...
		// 0) If there is a changed scrap, try to find from there
		if (mState.isPreLayout()) {
   		 holder = getChangedScrapViewForPosition(position);
   		 fromScrapOrHiddenOrCache = holder != null;
		}
		...
}

public boolean isPreLayout() {
     return mInPreLayout;
}
        
private void dispatchLayoutStep1() {
..
   mState.mInPreLayout = mState.mRunPredictiveAnimations;
...
}

private void processAdapterUpdatesAndSetAnimationFlags() {
...
    mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
        && animationTypeSupported
        && !mDataSetHasChangedAfterLayout
        && predictiveItemAnimationsEnabled();
}

由上代码调用知道preLayout的状态受到predictiveItemAnimationsEnabled方法影响,因此如果需要预布局,我们需要重写LinearLayoutManager的supportsPredictiveItemAnimations方法返回true(默认为false),才会从mChangedScrap取缓存。

参考文档

http://frogermcs.github.io/recyclerview-animations-androiddevsummit-write-up/

https://juejin.cn/post/6844904146684870669

预布局核心原理:
LinearLayoutManager.java

private void dispatchLayoutStep1() {
        ...
        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;

            ...
            // we don't process disappearing list because they may re-appear in post layout pass.
            clearOldPositions();
        } else {
            clearOldPositions();
        }
        onExitLayoutOrScroll();
        stopInterceptRequestLayout(false);
        mState.mLayoutStep = State.STEP_LAYOUT;
    }

当删除一个可见的item时,额外增加一个将要显示的item,这样item进入的时候就可以滑入而不是渐变的方式展现

1.1.2 ArrayList mAttachedScrap

1.1.2.1 缓存存入时机

见1.1.1.1节

1.1.2.2缓存取出时机

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
   ...             
		// 1) Find by position from scrap/hidden list/cache
		if (holder == null) {
  		  holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
		}
}
1.1.3 getScrapOrCachedViewForId

通过id取缓存,该机制只对设置了Adapter.hasStableIds()为true,并且配合重写getItemId(int position) 方法才会生效,可以解决一些数据刷新图片闪烁的问题(网上的说法,暂时还没验证)

1.2、滚动复用

1.2.1 ArrayList mCachedViews

1.2.1.1

屏幕外的第一级缓存,

int mViewCacheMax = DEFAULT_CACHE_SIZE;
...
static final int DEFAULT_CACHE_SIZE = 2;

如果支持预取操作(最新版默认开启,开发者可以自行选择关闭)

GapWorker.java
void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
...
        if (nested) {
         ..
                layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this);
        } else {
        ..
                layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
                        view.mState, this);
        }

        if (mCount > layout.mPrefetchMaxCountObserved) {
            layout.mPrefetchMaxCountObserved = mCount;
            layout.mPrefetchMaxObservedInInitialPrefetch = nested;
            view.mRecycler.updateViewCacheSize();
        }
}

Recycler.java
void updateViewCacheSize() {
    int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
    mViewCacheMax = mRequestedCacheMax + extraCache;
...
}

如果recyclerview支持嵌套,用户可以设置预取的个数,走collectInitialPrefetchPositions逻辑,默认情况下走collectAdjacentPrefetchPositions逻辑,mCount++设置为1,从而赋值给mPrefetchMaxCountObserved,所以一级缓存默认情况下最大值是3

1.2.1.2 预取机制

参考文档

https://blog.csdn.net/weishenhong/article/details/81150172

https://blog.csdn.net/crazy_everyday_xrp/article/details/70344638?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

https://medium.com/google-developers/recyclerview-prefetch-c2f269075710(需要科学上网)

GapWorker预取加载原理:将闲置的UI线程利用起来,在UI线程将页面数据交由Render线程渲染以后提前处理下一个显示的item的数据创建和绑定。
1.2.2 ViewCacheExtension

屏幕外的第二级缓存,开发者可以自定义的缓存,在RecycledViewPool前,CachedViews之后。

1.2.3 RecycledViewPool

屏幕外的第三级缓存

private static final int DEFAULT_MAX_SCRAP = 5;

RecycledViewPool默认最大缓存为5个

if (holder == null) { // fallback to pool
...
    holder = getRecycledViewPool().getRecycledView(type);
    if (holder != null) {
        holder.resetInternal();
        if (FORCE_INVALIDATE_DISPLAY_LIST) {
            invalidateDisplayListInt(holder);
        }
    }
}

void resetInternal() {
    mFlags = 0;
    ...
}
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);
}

tryBindViewHolderByDeadline会回调onBindViewHolder逻辑,由判断条件知道,从RecycledViewPool取出的holder会清理标志位,!holder.isBound()为true,所以从RecycledViewPool取出的holder会重新走onBindViewHolder逻辑,

状态 场景
holder.isBound() 回调onBindViewHolder前会设置isBound为true
holder.needsUpdate() onItemRangeChanged进行更新时会触发,对应某个item更新
holder.isInvalid() notifyDataSetChanged被调用,对应整个数据集刷新
/**
 * This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType
 * are all valid.
 */
static final int FLAG_BOUND = 1 << 0;

/**
 * The data this ViewHolder's view reflects is stale and needs to be rebound
 * by the adapter. mPosition and mItemId are consistent.
 */
static final int FLAG_UPDATE = 1 << 1;

/**
 * This ViewHolder's data is invalid. The identity implied by mPosition and mItemId
 * are not to be trusted and may no longer match the item view type.
 * This ViewHolder must be fully rebound to different data.
 */
static final int FLAG_INVALID = 1 << 2;

一些api分析:

1、setHasFixedSize:
        @Override
        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
            assertNotInLayoutOrScroll(null);
            if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
                triggerUpdateProcessor();
            }
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            assertNotInLayoutOrScroll(null);
            if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
                triggerUpdateProcessor();
            }
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            assertNotInLayoutOrScroll(null);
            if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
                triggerUpdateProcessor();
            }
        }

        @Override
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
            assertNotInLayoutOrScroll(null);
            if (mAdapterHelper.onItemRangeMoved(fromPosition, toPosition, itemCount)) {
                triggerUpdateProcessor();
            }
        }

        void triggerUpdateProcessor() {
            if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
                ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
            } else {
                mAdapterUpdateDuringMeasure = true;
                requestLayout();
            }
        }

如果设置setHasFixedSize为true,通过onItemRangeInserted、onItemRangeRemoved等方法更新列表时不会重新requestLayout,提升性能