RecyclerView源碼分析二之繪製與複用

介紹

我們都知道在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源碼分析一之簡單介紹
下一篇RecyclerView源碼分析三之動畫分析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章