在開始緩存前缨称,我們先從RecyclerView的繪制開始分析,都知道RecyclerView的繪制是在LayoutManager中祝迂,真正執(zhí)行LayoutManager繪制的地方dispatchLayoutStep2()睦尽,同樣,放上代碼:
private void dispatchLayoutStep2() {
...
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
...
}
mLayout就是LayoutManager型雳,以LinearLayoutManager為例当凡,跟進去看onLayoutChildren方法做了什么:
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 != RecyclerView.NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
return;
}
}
if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
}
ensureLayoutState();
mLayoutState.mRecycle = false;
// 1. 確定布局方向
resolveShouldLayoutReverse();
final View focused = getFocusedChild();
//2. 計算錨點位置
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.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())) {
mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
}
if (DEBUG) {
Log.d(TAG, "Anchor info:" + mAnchorInfo);
}
...
//3. item 放入緩存
detachAndScrapAttachedViews(recycler);
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
// noRecycleSpace not needed: recycling doesn't happen in below's fill
// invocations because mScrollingOffset is set to SCROLLING_OFFSET_NaN
mLayoutState.mNoRecycleSpace = 0;
if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = 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.mExtraFillSpace = 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.mExtraFillSpace = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
// 4. 具體填充方法
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.mExtraFillSpace = 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.mExtraFillSpace = 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();
}
}
代碼1處解決布局方向,實際就是縱向還是橫向布局纠俭;代碼2 處在錨點信息已過期或者滾動位置不是初始位置沿量,或者預存儲狀態(tài)不為null,則重置錨點冤荆,默認mAnchorInfo.mValid 為false朴则,所以會進入if邏輯中,mLayoutFromEnd的值钓简,在VERTICAL下mShouldReverseLayout為false 佛掖,mStackFromEnd默認也為false,所以異或的結果mLayoutFromEnd為false涌庭,其中mStackFromEnd是用來確定是否為正向布局,簡單來說在VERTICAL下mStackFromEnd為false為從上到下欧宜,為true反之坐榆。代碼3處放入緩存,稍后分析冗茸;先看一下確定錨點后怎么填充的席镀,具體填充方法就是代碼4處,看fill是如何做的:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
return start - layoutState.mAvailable;
}
fill 核心的就是調(diào)用layoutChunk方法夏漱,點進去繼續(xù)看:
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;
}
RecyclerView.LayoutParams params = (RecyclerView.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();
}
這個方法就是關鍵的布局了豪诲,首先是調(diào)用layoutState.next(recycler)方法,這個方法就是在緩存中取出需要的view挂绰,然后繼續(xù)就是往ViewGroup中addview屎篱,畢竟RecyclerView還是繼承自ViewGroup。之后就是測量子view的寬高,在layout到指定位置交播。
到此重虑,我們在總結一下繪制的過程:
RecyclerView的繪制是由LayoutManager來處理,處理過程中先確認布局方向秦士,在根據(jù)布局方向確定錨點位置缺厉,之后是在確定填充方向(toStart 、toEnd)隧土,最后先存入緩存再在緩存中拿到子View提针,在addView到ViewGroup中
四級緩存
在繪制流程中,會將子view放置到緩存中曹傀,緩存怎么處理的辐脖,又有哪些緩存?首先卖毁,關于緩存都知道其存在四級緩存揖曾,先分別介紹各個緩存:
緩存級別 | 對應屬性 | 含義 |
---|---|---|
一級緩存 | mAttachedScrap和mChangedScrap | detach的view相關, mAttachedScrap存儲的是當前還在屏幕中的ViewHolder亥啦。mChangedScrap存儲的是數(shù)據(jù)被更新的ViewHolder 炭剪,通常在預布局中使用。 |
二級緩存 | mCachedViews | 默認大小為2翔脱,remove掉的view會先進入此緩存 |
三級緩存 | mViewCacheExtension | 需自定義的奴拦,用不到 |
四級緩存 | mRecyclerPool | 按照viewType緩存holder,每個type對應的緩存大小為5 |
需要注意的是一級緩存跟detach的view相關届吁,其他是remove掉的错妖,這兩個的區(qū)別可以看這篇文章
簡單的介紹之后,我們還是在源碼角度看看怎么做的疚沐,還是在onLayoutChildren方法中暂氯,看到在找到錨點后會執(zhí)行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);
}
}
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
首先得到child數(shù)量亮蛔,這地方需要注意的是getChildCount()
并不等同于adapter.getItemCount()
,而是attach到recyclerView的數(shù)量或者可以看的到的child痴施。其次就是scrapOrRecycleView開始放入緩存,我們看if邏輯判斷究流,當viewHolder是invalid狀態(tài)并且沒有移除且沒有設置StableIds標識會執(zhí)行removeView辣吃,invalid的狀態(tài)改變,在分析notifyDataSetChanged時在說芬探,這地方直接看else的代碼神得,先detach掉在scrapView,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);
}
}
當調(diào)用notifyDataSetChanged時ViewHolder.FLAG_INVALID和holder.isUpdated()為true偷仿,所以會進入if控制中哩簿,然后就是添加到一級緩存mAttachedScrap中宵蕉,總結一下剛才的過程:
detachAndScrapAttachedViews 會將正在顯示的View 存入到一級緩存mAttachedScrap。
放入之后在什么地方會用到卡骂,回到layoutChunk方法国裳,第一行調(diào)用了View view = layoutState.next(recycler);現(xiàn)在看看是怎么取到View的:
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
調(diào)用了recycler.getViewForPosition(mCurrentPosition);,跟進去最后會調(diào)用到tryGetViewHolderForPositionByDeadline:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) 預布局情況從mChangedScrap緩存中取
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) {
// 無效會重新放入緩存中
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;
}
}
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
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) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
//3)mViewCacheExtension 獲取
if (holder == null && mViewCacheExtension != null) {
...
}
if (holder == null) { // fallback to pool
...
//4) 從RecycledViewPool中獲取
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
long start = getNanoTime();
...
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
...
return holder;
}
1處調(diào)用getScrapOrHiddenOrCachedHolderForPosition全跨,這個方法做的事情很簡單就是依次從mAttachedScrap缝左、hiddenViews和mCachedViews中獲取holder,需要注意的hiddenViews并不算緩存浓若,這個只和動畫有關渺杉,這個以后有時間在理解,2處會判斷是否設置了StableIds挪钓,設置了就調(diào)用getScrapOrCachedViewForId方法是越,這個方法會依次從mAttachedScrap和mCachedViews中獲取holder,getScrapOrHiddenOrCachedHolderForPosition
和getScrapOrCachedViewForId
調(diào)用都會在mAttachedScrap和mCachedViews中獲取holder碌上,不同的地方是一個通過position倚评,一個通過Id,也就是說通過position直接拿到的holder不用去判斷ItemViewType是否一致馏予,通過Id需要判斷ItemViewType的類型是否一致天梧。4處就是在RecycledViewPool中獲取了,具體看看這里面如何拿到的:
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
這地方先通過ItemViewType拿到指定類型的緩存霞丧,然后在得到指定的holder呢岗。
最后如果RecycledViewPool中沒有,就調(diào)用mAdapter.createViewHolder
問題
- notifyItemChanged 和notifyDataSetChanged區(qū)別
我們看各自源碼做了什么
@Override
//notifyDataSetChanged
public void onChanged() {
assertNotInLayoutOrScroll(null);
mState.mStructureChanged = true;
processDataSetCompletelyChanged(true);
if (!mAdapterHelper.hasPendingUpdates()) {
requestLayout();
}
}
//notifyItemChanged
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
triggerUpdateProcessor();
}
}
void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
markKnownViewsInvalid();
}
void markKnownViewsInvalid() {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && !holder.shouldIgnore()) {
holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
}
}
markItemDecorInsetsDirty();
mRecycler.markKnownViewsInvalid();
}
不一樣的地方就是notifyDataSetChanged最后會添加flag值ViewHolder.FLAG_UPDATE和ViewHolder.FLAG_INVALID蛹尝,這兩個的作用是什么后豫,我們回到緩存相關的代碼
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
看到會在if判斷中執(zhí)行,也就是會先remove掉view突那,在放入二級緩存中挫酿,這個復用性要比detach的低一些,具體可以參考這篇文章愕难,所以remove掉在add回來可能就會有圖片閃動的問題饭豹,這也是notifyDataSetChanged效率低的原因。
- 如何提高緩存復用或StableIds 如何提高效率的
前面也說過务漩,notifyDataSetChanged會效率低些,不過在重寫mAdapter.hasStableIds()時它褪,可以讓緩存放入一級緩存中饵骨,另外在復用時通過ViewType方式來獲取ViewHolder,會優(yōu)先到一級或者二級緩存里面去尋找茫打,而不是直接去RecyclerViewPool里面去尋找居触。
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
}
參考:
RecyclerView 源碼分析(三) - RecyclerView的緩存機制
RecyclerView機制分析: Recycler