疑问
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://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,提升性能