RecyclerView缓存策略
首先,我们还是看一下RecycerView典型的Adapter的实现:
public class MyRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerviewAdapter.MyViewHolder> {
private Context context;
private List<String> data;
public MyRecyclerViewAdapter(Context context,List<String> data){
this.context = context;
this.data = data;
}
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.recycler_item_my, parent, false);
return new MyViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, final int position) {
holder.name.setText(data.get(position));
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("这里是点击每一行item的响应事件",""+position+item);
}
});
}
@Override
public int getItemCount() {
return data.size();
}
public class MyViewHolder extends RecyclerView.ViewHolder{
TextView name;
public MyViewHolder(View itemView) {
super(itemView);
name = itemView.findViewById(R.id.name);
}
}
}
复制代码
可以看到Adapter主要是两个方法在缓存中起作用:用来创建新的ViewHolder的onCreateViewHolder,以及用来显示数据的onBindViewHolder方法。
2.1 Recycler
同ListView中的RecycleBin一样,RecyclerView中的缓存也由一个内部类Recycler进行管理。Recycler里面有四个不同层次的缓存,比ListView层次要丰富一点,当然,这与RecyclerView拓展性更好有一定的关系。
Recycler里面字段如下:
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;
复制代码
解释如下:
-
mAttachedScrap、mChangedScrap
一级缓存,同ListView中ActionViews,在layout发生前将屏幕上面的ViewHolder保存起来,供layout中进行复用
-
mCachedViews
二级缓存,默认大小保持在DEFAULT_CACHE_SIZE = 2,可以通过RecyclerView.setItemViewCacheSize(int)方法进行设置 mCachedViews数量如果超出限制,会根据索引将里面旧的移动到RecyclerViewPool中
-
ViewCacheExtension
三级缓存,开发者可以自定义的缓存
-
RecyclerViewPool
四级缓存,可以在多个RecyclerView中共享缓存
根据ViewType来缓存ViewHolder,每个ViewType的数组大小默认为DEFAULT_MAX_SCRAP = 5,超过部分会丢弃,可以通过其setMaxRecycledViews(int viewType, int max)方法来控制对应type的缓存池大小。
Recycler的方法本质上就是对上面数据结构的一些操作。主要的方法有:
-
recycleView(View)
将view对应的ViewHolder移动到mCachedViews中;如果View是scrapped状态,会先unscrap
-
recycleViewHolderInternal(ViewHolder)
将ViewHolder保存到mCachedViews中
-
addViewHolderToRecycledViewPool(ViewHolder, boolean)
将ViewHolder保存到RecycledViewPool中
-
scrapView(View)
将一个attached状态的View保存到mAttachedScrap或mChangedScrap中
-
getChangedScrapViewForPosition(int)
从mChangedScrap中寻找匹配的ViewHolder
-
getScrapOrHiddenOrCachedHolderForPosition(int, boolean)
依次从mAttachedScrap、mCachedViews中寻找匹配的ViewHolder
-
getScrapOrCachedViewForId(long, int, boolean)
依次从mAttachedScrap、mCachedViews中寻找匹配的ViewHolder
-
tryGetViewHolderForPositionByDeadline(int, boolean, long)
从mChangedScrap、mAttachedScrap、mCachedViews、ViewCacheExtension、RecycledViewPool中进行匹配;若匹配不了,最后会直接调用Adapter.createViewHolder方法进行创建
-
tryBindViewHolderByDeadline(ViewHolder, int, int, long)
调用Adapter.bindViewHolder方法绑定View
2.2 缓存流程
RecyclerView的缓存流程同ListView一样,也是体现在了layout过程中。由于RecyclerView layout过程中步骤比较多,而这些内容不是本章的重点,所以这里只给出大致流程,重点放到缓存流程中。
RecyclerView的layout流程分为三个方法,对应layout step的三个步骤。这部分表格如下:
本章中我们关注的重点显然是在dispatchLayoutStep2方法中:
/**
* 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() {
eatRequestLayout();
onEnterLayoutOrScroll();
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mPendingSavedState = null;
// onLayoutChildren may have caused client code to disable item animations; re-check
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
resumeRequestLayout(false);
}
复制代码
该方法比较简单,重点在第15行的mLayout.onLayoutChildren(mRecycler, mState)方法中。这里mLayout我们选择最常用的LinearLayoutManager进行分析。LinearLayoutManager.onLayoutChildren方法如下:
/**
* {@inheritDoc}
*/
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// layout algorithm:
// 1) by checking children and other variables, find an anchor coordinate and an anchor
// item position.
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
// create layout state
if (DEBUG) {
Log.d(TAG, "is pre layout:" + state.isPreLayout());
}
if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
return;
}
}
if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
}
ensureLayoutState();
mLayoutState.mRecycle = false;
// resolve layout direction
resolveShouldLayoutReverse();
final View focused = getFocusedChild();
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
} else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
>= mOrientationHelper.getEndAfterPadding()
|| mOrientationHelper.getDecoratedEnd(focused)
<= mOrientationHelper.getStartAfterPadding())) {
// This case relates to when the anchor child is the focused view and due to layout
// shrinking the focused view fell outside the viewport, e.g. when soft keyboard shows
// up after tapping an EditText which shrinks RV causing the focused view (The tapped
// EditText which is the anchor child) to get kicked out of the screen. Will update the
// anchor coordinate in order to make sure that the focused view is laid out. Otherwise,
// the available space in layoutState will be calculated as negative preventing the
// focused view from being laid out in fill.
// Note that we won't update the anchor position between layout passes (refer to
// TestResizingRelayoutWithAutoMeasure), which happens if we were to call
// updateAnchorInfoForLayout for an anchor that's not the focused view (e.g. a reference
// child which can change between layout passes).
mAnchorInfo.assignFromViewAndKeepVisibleRect(focused);
}
if (DEBUG) {
Log.d(TAG, "Anchor info:" + mAnchorInfo);
}
// LLM may decide to layout items for "extra" pixels to account for scrolling target,
// caching or predictive animations.
int extraForStart;
int extraForEnd;
final int extra = getExtraLayoutSpace(state);
// If the previous scroll delta was less than zero, the extra space should be laid out
// at the start. Otherwise, it should be at the end.
if (mLayoutState.mLastScrollDelta >= 0) {
extraForEnd = extra;
extraForStart = 0;
} else {
extraForStart = extra;
extraForEnd = 0;
}
extraForStart += mOrientationHelper.getStartAfterPadding();
extraForEnd += mOrientationHelper.getEndPadding();
if (state.isPreLayout() && mPendingScrollPosition != NO_POSITION
&& mPendingScrollPositionOffset != INVALID_OFFSET) {
// if the child is visible and we are going to move it around, we should layout
// extra items in the opposite direction to make sure new items animate nicely
// instead of just fading in
final View existing = findViewByPosition(mPendingScrollPosition);
if (existing != null) {
final int current;
final int upcomingOffset;
if (mShouldReverseLayout) {
current = mOrientationHelper.getEndAfterPadding()
- mOrientationHelper.getDecoratedEnd(existing);
upcomingOffset = current - mPendingScrollPositionOffset;
} else {
current = mOrientationHelper.getDecoratedStart(existing)
- mOrientationHelper.getStartAfterPadding();
upcomingOffset = mPendingScrollPositionOffset - current;
}
if (upcomingOffset > 0) {
extraForStart += upcomingOffset;
} else {
extraForEnd -= upcomingOffset;
}
}
}
int startOffset;
int endOffset;
final int firstLayoutDirection;
if (mAnchorInfo.mLayoutFromEnd) {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
: LayoutState.ITEM_DIRECTION_HEAD;
} else {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
: LayoutState.ITEM_DIRECTION_TAIL;
}
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
detachAndScrapAttachedViews(recycler);
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
final int firstElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForEnd += mLayoutState.mAvailable;
}
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
// end could not consume all. add more items towards start
extraForStart = mLayoutState.mAvailable;
updateLayoutStateToFillStart(firstElement, startOffset);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
// start could not consume all it should. add more items towards end
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
// changes may cause gaps on the UI, try to fix them.
// TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have
// changed
if (getChildCount() > 0) {
// because layout from end may be changed by scroll to position
// we re-calculate it.
// find which side we should check for gaps.
if (mShouldReverseLayout ^ mStackFromEnd) {
int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
startOffset += fixOffset;
endOffset += fixOffset;
fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
startOffset += fixOffset;
endOffset += fixOffset;
} else {
int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
startOffset += fixOffset;
endOffset += fixOffset;
fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
startOffset += fixOffset;
endOffset += fixOffset;
}
}
layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
if (!state.isPreLayout()) {
mOrientationHelper.onLayoutComplete();
} else {
mAnchorInfo.reset();
}
mLastStackFromEnd = mStackFromEnd;
if (DEBUG) {
validateChildOrder();
}
}
复制代码
方法很长,但还好有一些注释。
-
首先,从开头到第112行都是第一步的内容:计算锚点坐标以及锚点item的position。谁让112行是onAnchorReady方法呢,太明显了。
-
注意第113行的detachAndScrapAttachedViews方法,该方法会对所有的子View调用scrapOrRecycleView方法。这样所有的子View都会暂时detach掉,并保存到mAttachedScrap或mChangedScrap或mCachedViews中,等待后续复用。
/**
* Temporarily detach and scrap all currently attached child views. Views will be scrapped
* into the given Recycler. The Recycler may prefer to reuse scrap views before
* other views that were previously recycled.
*
* @param recycler Recycler to scrap views into
*/
public void detachAndScrapAttachedViews(Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
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);
}
}
复制代码
前面提到过mCachedViews如果空间不足,会根据索引将里面旧的移动到RecyclerViewPool中,这样此方法的就将除了ViewCacheExtension之外的缓存全部囊括了。
- 根据计算的值,多次调用fill方法填充子View。
显然,fill方法是新重点。该方法和ListView中的fillDown等类似,也是循环计算-填充-计算,我们直接看填充部分。填充部分调用了layoutChunk方法:该方法会首先调用LayoutState.next方法获取一个view;然后会addView,add过程中如果是detach过的,将会view重新attach到RecyclerView上,否则就是remove过了的,直接addView;最后调用measureChildWithMargins、layoutDecoratedWithMargins方法对子View进行测量、布局。layoutChunk方法代码如下:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
if (view == null) {
if (DEBUG && layoutState.mScrapList == null) {
throw new RuntimeException("received null view when unexpected");
}
// if we are laying out views in scrap, this may return null which means there is
// no more items to layout.
result.mFinished = true;
return;
}
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getWidth() - getPaddingRight();
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = layoutState.mOffset - result.mConsumed;
} else {
top = layoutState.mOffset;
bottom = layoutState.mOffset + result.mConsumed;
}
} else {
top = getPaddingTop();
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
right = layoutState.mOffset;
left = layoutState.mOffset - result.mConsumed;
} else {
left = layoutState.mOffset;
right = layoutState.mOffset + result.mConsumed;
}
}
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
layoutDecoratedWithMargins(view, left, top, right, bottom);
if (DEBUG) {
Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
}
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
复制代码
很显然,缓存部分的关键就是LayoutState.next方法了:
/**
* Gets the view for the next element that we should layout.
* Also updates current item index to the next item, based on {@link #mItemDirection}
*
* @return The next element that we should layout.
*/
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
复制代码
我们先略过mScrapList,暂时认为其为null,后面遇到再分析。所以这里调用了RecyclerView.getViewForPosition方法:
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
复制代码
离真相又近了一步,tryGetViewHolderForPositionByDeadline方法里面会对各级缓存进行匹配,这里分段进行解释。
a. 如果有mChangedScrap,尝试进行匹配
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
复制代码
这里的isPreLayout()与mState.mRunPredictiveAnimations有直接关系,可以看成前者的值取决与后者,该值在dispatchLayoutStep1过程中被更新;当Item发生了更新时,scrapView方法会将ViewHolder保存到mChangedScrap中去。
b. 尝试从mAttachedScrap、mCachedViews中寻找匹配的ViewHolder。找到之后会对ViewHolder做一些检查,如果不满足条件,且dryRun为false(实际上就是false),会将ViewHolder清除掉并保存到mCachedViews中。在向mCachedViews中添加缓存时,如果超过了允许的上限(即mViewCacheMax),将会把旧的缓存移动到RecycledViewPool中。
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
复制代码
c. 如果Adapter.hasStableIds()为true,会根据ItemId和ViewType在mAttachedScrap、mCachedViews中寻找ViewHolder。Adapter中该属性默认为false。
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
复制代码
d. 如果存在ViewCacheExtension,调用ViewCacheExtension.getViewForPositionAndType寻找ViewHolder
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) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view which does not have a ViewHolder"
+ exceptionLabel());
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view." + exceptionLabel());
}
}
}
复制代码
e. fallback到RecycledViewPool,看是否有可用的ViewHolder
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
复制代码
f. 以上都不满足,最后调用Adapter.createViewHolder创建ViewHolder
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
holder = mAdapter.createViewHolder(RecyclerView.this, type);
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
RecyclerView innerView = findNestedRecyclerView(holder.itemView);
if (innerView != null) {
holder.mNestedRecyclerView = new WeakReference<>(innerView);
}
}
long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
}
}
复制代码
在获取到ViewHolder之后,如果需要bind,会调用tryBindViewHolderByDeadline方法,该方法中接着调用Adapter.bindViewHolder方法交给开发者完成绑定工作。
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);
}
复制代码
tryGetViewHolderForPositionByDeadline方法完成之后会一直返回到LinearLayoutManager.layoutChunk方法中,接着会根据ViewHolder的来源,该attach的attach,该addView的addView,最后measure并layout,一个子View的layout过程就完成了。
最后以一张流程图结束本节:
ListView与RecyclerView缓存机制对比
ListView与RecyclerView缓存机制原理大致相似:滑动过程中,离屏的ItemView即被回收至缓存,入屏的ItemView则会优先从缓存中获取,只是ListView与RecyclerView的实现细节有差异.(这只是缓存使用的其中一个场景,还有如刷新等)。原理图如下所示:
两者缓存机制的对比有以下几点不同:
-
层级不同
RecyclerView比ListView多两级缓存,支持多个离屏ItemView缓存,支持开发者自定义缓存处理逻辑,支持所有RecyclerView共用同一个RecyclerViewPool(缓存池)。
ListView和RecyclerView缓存机制基本一致:
a. mActiveViews和mAttachedScrap功能相似,用来快速重用屏幕上可见的列表项,而不需要重新创建和绑定
b. mScrapViews和mCacheViews + mRecyclerPool功能相似,用来缓存离开屏幕的itemView,让即将进入屏幕的itemView复用
c. RecyclerView的优势在于:
- mCacheViews的使用,可以做到屏幕外的列表项在进入屏幕时也无须bindView快速重用
- mRecyclerPool可以供多个RecyclerView共同使用,在特定场景下(如ViewPager+多个列表页,或者竖向列表的item中嵌套有横向列表等)有优势。
复制代码
客观来说,RecyclerView在特定场景下对ListView的缓存机制做了补强和完善。
- 缓存不同
- RecyclerView缓存RecyclerView.ViewHolder,抽象可理解为:View + ViewHolder
- ListView缓存View,实际使用的时候需要手动将自定义的ViewHolder添加到View的tag中
缓存不同,二者在缓存的使用上也略有差异,具体来说:
-
RecyclerView中mCacheViews获取时,是通过匹配pos获取目标位置的换缓存的,这样做的好处是,当数据源不变的情况下,无须重新bindView;而同样是离屏缓存,ListView从mScrapViews根据pos获取相应的缓存,但是并没有直接使用,而是重新调用了Adapter的getView方法,这就必定会导致我们的bind代码执行。
-
ListView中通过pos获取的是View;RecyclerView通过pos获取的是ViewHolder。
另外,RecyclerView更大的亮点在于提供了局部刷新的接口,这样可以避免调用许多无用的bindView。
ListView和RecyclerView最大的区别在于数据源改变时的缓存的处理逻辑,ListView是”一锅端”,将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。