介紹
我們都知道在Android ViewGroup的繪製流程中,需要通過重寫方法onMeasure(int width,int height) 和 onLayout(int l,int t,int r,int b)來實現自定定義ViewGroup的。
RecyclerView的測量、繪製依然是依靠這兩個方法來實現的。
測量與繪製
測量
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
//LinearLayoutManager默認開啓'自動測量'
//onMeasure會執行兩次
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
//通過mLayout設置大小參數給recyclerview
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
//如果RecyclerView的寬高都是精確模式(Match_Parent或者精確的dp),則結束本方法的執行,接下來等待onLayout方法的執行
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
//進行第一步繪製(後面會詳細講到)
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
//更新寬高信息到mLayout對象,mLayout是一個LayoutManager的實例
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
//進行真正的繪製佈局操作
dispatchLayoutStep2();
//根據children的佈局大小再做一次,寬高決策
//如果是精確,則取精確對應的值
//如果是AT_MOST,則取 size和(disired,min中的最大值)的最小值
// 否則 disired,min中的最大值)
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
//如果ReyclerView沒有精確的寬高,且它的children中至少有一個item寬和高不精確(針對LinearLayoutManager)
//則需要進行二次測量
if (mLayout.shouldMeasureTwice()) {
//RecyclerView先將自己的寬高設置爲精確模式
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
//再進行二次佈局
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
...
}
}
/**
* 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() {
mState.assertLayoutStep(State.STEP_START);
//記錄需要滾動的距離到State中
fillRemainingScrollValues(mState);
mState.mIsMeasuring = false;
//優化requestLayout的調用
startInterceptRequestLayout();
//記錄View動畫信息的數據結果,清除緩存信息
mViewInfoStore.clear();
//標記滾動或者佈局階段,在此期間 某些方法不能被調用
onEnterLayoutOrScroll();
//處理Adapter更新內容和設置動畫標記
processAdapterUpdatesAndSetAnimationFlags();
//保存焦點信息
saveFocusInfo();
mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
mItemsAddedOrRemoved = mItemsChanged = false;
mState.mInPreLayout = mState.mRunPredictiveAnimations;
mState.mItemCount = mAdapter.getItemCount();
//記錄目前最大最小position
findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
//processAdapterUpdatesAndSetAnimationFlags方法中設置的標記
//是否需要執行動畫,該值在processAdapterUpdatesAndSetAnimationFlags()方法中進行設置
if (mState.mRunSimpleAnimations) {
//下一篇介紹動畫相關內容
....
}
//processAdapterUpdatesAndSetAnimationFlags方法中設置的標記
//是否需要執行預測動畫,該值在processAdapterUpdatesAndSetAnimationFlags()方法中進行設置
if (mState.mRunPredictiveAnimations) {
//下一篇介紹動畫相關內容
...
//進行原有位置信息的清除操作
clearOldPositions();
} else {
//進行原有位置信息的清除操作
clearOldPositions();
}
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
//設置狀態爲“佈局”階段
mState.mLayoutStep = State.STEP_LAYOUT;
}
/**
* 真正的佈局child的操作
*/
private void dispatchLayoutStep2() {
...
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
//將更新操作一次性 觸發
mAdapterHelper.consumeUpdatesInOnePass();
//設置一下mState參數信息,便於在佈局階段使用
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
// Step 2: Run layout
//進入佈局階段
mState.mInPreLayout = false;
//真正的佈局操作,交由LayoutManager去實現,比如LinearLayoutManager
mLayout.onLayoutChildren(mRecycler, mState);
...
}
佈局
咱們接着上面的內容,來看一下LinearLayoutManager的是如何佈局RecyclerView的children的。
/**
* LinearLayoutManager.java
*/
@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.
final View focused = getFocusedChild();
//計算anchor的位置和座標信息
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0
? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
//已佔用空間和佈局方向
calculateExtraLayoutSpace(state, mReusableIntPair);
int extraForStart = Math.max(0, mReusableIntPair[0])
+ mOrientationHelper.getStartAfterPadding();
int extraForEnd = Math.max(0, mReusableIntPair[1])
+ mOrientationHelper.getEndPadding();
//錨點位置確定
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
//暫時回收attchedView
detachAndScrapAttachedViews(recycler);
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
mLayoutState.mNoRecycleSpace = 0;
if (mAnchorInfo.mLayoutFromEnd) {
....
} else {
//以mAnchorInfo爲參照物 向後佈局
//將mAnchorInfo傳遞給mLayoutState對象,便於佈局
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
//開始佈局
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
//以mAnchorInfo爲參照物 向前佈局
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
...
//預佈局動畫(在下一篇會詳細介紹)
layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
if (!state.isPreLayout()) {
mOrientationHelper.onLayoutComplete();
} else {
mAnchorInfo.reset();
}
mLastStackFromEnd = mStackFromEnd;
if (DEBUG) {
validateChildOrder();
}
}
/**
* LinearLayoutManager.java
*/
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
//循環佈局
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
//佈局item
layoutChunk(recycler, state, layoutState, layoutChunkResult);
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
//重新計算剩餘空間
remainingSpace -= layoutChunkResult.mConsumed;
}
}
return start - layoutState.mAvailable;
}
/**
* LinearLayoutManager.java
*/
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//獲取一個ItemView
//注意這裏 實現了複用操作,具體的複用邏輯,再下一小節分析
View view = layoutState.next(recycler);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
//如果mScrapList爲空,則正常即可
if (layoutState.mScrapList == null) {
....
addView(view, 0);
....
} else {
...
//否則 需要記錄消失的動畫信息
//再下一篇將進行分析
addDisappearingView(view);
...
}
//測量itemView的大小
measureChildWithMargins(view, 0, 0);
//每個item消耗的大小(橫向或者縱向)
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
//重新計算帶有ItemDecoration的大小
layoutDecoratedWithMargins(view, left, top, right, bottom);
//如果當前的Item是被刪除和更改的,則不消耗空間
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
接下來咱們分析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;
}
void dispatchLayout() {
//寬高都精確,執行該條件
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
//如果高度發生變化,需要重新測量和佈局
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
//將測量規格設置爲“EXACTLY”
mLayout.setExactMeasureSpecsFrom(this);
}
//觸發必要的動畫和必要的資源回收
dispatchLayoutStep3();
}
private void dispatchLayoutStep3() {
//當前動畫執行階段
mState.assertLayoutStep(State.STEP_ANIMATIONS);
...
//重置階段的狀態
mState.mLayoutStep = State.STEP_START;
if (mState.mRunSimpleAnimations) {
//下一篇介紹動畫相關內容
...
}
//如何確保回收地 不重複 不漏掉
mLayout.removeAndRecycleScrapInt(mRecycler);
...
if (mRecycler.mChangedScrap != null) {
mRecycler.mChangedScrap.clear();
}
//佈局完成,重置LayoutManager狀態
mLayout.onLayoutCompleted(mState);
...
//恢復焦點信息
recoverFocusFromState();
resetFocusInfo();
}
緩存與複用機制
複用
ReyclerView中的複用機制是通過其內部類Recycler來實現
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
}
共有四級緩存:
- mAttachedScrap 和 mChangedScrap
- mAttachedScrap存儲當前屏幕中的ViewHolder,並且可以根據id和position進行查找
- mChangedScrap 用來緩存數據已經發生變化的,需要改變的ViewHolder。
- mCachedViews
- 用來緩存移除到屏幕中的ViewHolder,默認緩存容量爲2.通過setViewCacheSize可以改變。緩存策略FIFO
- ViewCacheExtension
- 開發者用戶自定義的緩存策略
- ReycledViewPool
- 如果mCachedViews中存儲不下ViewHolder,則會將其放在RecycledPool中。可以更加Type查找ViewHolder。緩存策略根據type緩存ViewHolder。並且多個recyclerview之間共享RecycledViewPool
接下來看一下在LinearLayoutManager中,是如何調用Recycler複用ViewHolder的。
-> LinearLayoutManager.layoutChunk(recycler, layoutState) // 循環調用,每次調用填充一個 ItemView 到 RV
-> LinearLayoutManager.LayoutState.next(recycler)
-> RecyclerView.Recycler.getViewForPosition(int)
-> Recycler.getViewForPosition(int, boolean) // 調用下面方法獲取 ViewHolder,並返回上面需要的 viewHolder.itemView
-> Recycler.tryGetViewHolderForPositionByDeadline(...)//真正獲取ViewHolder的地方
ViewHolder tryGetViewHolderForPositionByDeadline(int position, ...) {
if (mState.isPreLayout()) {
// 0) 預佈局從 mChangedScrap 裏面去獲取 ViewHolder,動畫相關
holder = getChangedScrapViewForPosition(position);
}
if (holder == null) {
// 1) 分別從 mAttachedScrap、 mHiddenViews、mCachedViews 獲取 ViewHolder
// 這個 mHiddenViews 是用來做動畫期間的複用
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
if (holder == null) {
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) 如果 Adapter 的 hasStableIds 方法返回爲 true
// 優先通過 ViewType 和 ItemId 兩個條件從 mAttachedScrap 和 mCachedViews 尋找
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.
// 3) 從自定義緩存獲取,別問,問就是別用
View view = mViewCacheExtension
getViewForPositionAndType(this, position, type);
holder = getChildViewHolder(view);
}
}
if (holder == null) {
// 4) 從 RecycledViewPool 獲取 ViewHolder
holder = getRecycledViewPool().getRecycledView(type);
}
if (holder == null) {
// 緩存全取過了,沒有,那隻好 create 一個咯
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
if (mState.isPreLayout() && holder.isBound()) {
...
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
...
//如果有必要,進行數據綁定操作
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
}
緩存
接下來,我們看一下,四級緩存之間是如何進行協調工作的。
mAttachedScrap和mChangedScrap
// 調用鏈
LinearLayoutManager.onLayoutChildren(...)
-> LayoutManager.detachAndScrapAttachedViews(recycler)
-> LayoutManager.scrapOrRecycleView(..., view)
-> Recycler.scrapView(view);
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
// 後面會講,緩存到 mCacheViews 和 RecyclerViewPool
recycler.recycleViewHolderInternal(viewHolder);
} else {
// 緩存到 scrap
recycler.scrapView(view);
}
}
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
// 標記爲移除或失效的 || 完全沒有改變 || item 無動畫或動畫不復用
mAttachedScrap.add(holder);
} else {
mChangedScrap.add(holder);
}
}
其實還有一種情況會調用到 scrapView , 從 mHiddenViews 獲得一個 ViewHolder 的話(發生在支持動畫的操作),會先將這個 ViewHolder 從 mHiddenViews 數組裏面移除,然後調用:
Recycler.tryGetViewHolderForPositionByDeadline(...)
-> Recycler.getScrapOrHiddenOrCachedHolderForPosition(...)
-> Recycler.scrapView(view)
mCacheViews和RecycledViewPool
void recycleViewHolderInternal(ViewHolder holder) {
if (forceRecycle || holder.isRecyclable()) {
if(mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
// 1. mCacheViews 滿了,最早加入的不要了放 RecyclerViewPool
recycleCachedViewAt(0);
}
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
// 2. 不能放進 mCacheViews 的放 RecyclerViewPool
addViewHolderToRecycledViewPool(holder, true);
}
}
}
// Recycles a cached view and removes the view from the list
void recycleCachedViewAt(int cachedViewIndex) {
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
addViewHolderToRecycledViewPool(viewHolder, true);
mCachedViews.remove(cachedViewIndex);
}
在這我們知道 recycleViewHolderInternal 會把 ViewHolder 緩存到 mCacheViews ,而不滿足加到 mCacheViews 的會緩存到 RecycledViewPool 。那又是什麼時候調用的 recycleViewHolderInternal 呢?有以下三種情況:
- 重新佈局,主要是調用 Adapter.notifyDataSetChange 且 Adapter 的 hasStableIds 方法返回爲 false 時調用。從這邊也可以看出爲什麼一般情況下 notifyDataSetChange 效率比其它 notifyXXX 方法低(使用二級緩存及優先級更低的緩存 ),同時也知道了,如果我們設置 Adapter.setHasStableIds(ture) 以及其它相關需要的實現,則可以提高效率(使用一級緩存)
- 在複用時,從一級緩存裏面獲取到 ViewHolder,但是此時這個 ViewHolder 已經不符合一級緩存的特點了(比如 Position 失效了,跟 ViewType 對不齊),就會從一級緩存裏面移除這個 ViewHolder,添加到這兩級緩存裏面
- 當調用 removeAnimatingView 方法時,如果當前 ViewHolder 被標記爲 remove ,會調用 recycleViewHolderInternal 方法來回收對應的 ViewHolder。調用 removeAnimatingView 方法的時機表示當前的 ItemAnimator 已經做完了
總結
到這裏,RecyclerView 的緩存複用機制應該分析完成了,總結一下:
- RecyclerView 的緩存複用機制,主要是通過內部類 Recycler 來實現
- Recycler 有 4 級緩存,每一級的緩存都有各自的作用,會按優先級使用。
- ViewHolder 會從某一級緩存移至其它級別的緩存
- mHideenViews 的存在是爲了解決在動畫期間進行復用的問題。
- 緩存複用 ViewHolder 時會針對內部不同的狀態 (mFlags) 進行相應的處理。
上一篇RecyclerView源碼分析一之簡單介紹
下一篇RecyclerView源碼分析三之動畫分析