1. 程式人生 > >【進階】RecyclerView原始碼解析(一)——繪製流程

【進階】RecyclerView原始碼解析(一)——繪製流程

引言

自從Google出了RecyclerView後,基本上列表的場景已經完全替代了原來的ListView和GridView,現在不僅僅是列表,多樣式(俗稱蓋樓),複雜頁面等,只要我們願意,RecyclerView幾乎可以代替實現80%的佈局,GitHub可以發現各種各樣給予RecyclerView的開源庫,無論是Adapter還是LayoutManager等。阿里對應出的vlayout其實也是RecyclerView的深度拓展,所以最近感覺僅僅瞭解用法是不夠的,使用RecyclerView越多,就越會發現這個元件的深度不能僅僅停留在使用層面,從RecyclerView可以延伸出一系列進階的使用、拓展、優化、封裝等,所以打算從原始碼角度來看一下RecyclerView的實現機制,更方便我們進行進階學習使用RecyclerView。

帶著問題看原始碼

最早的看原始碼可能帶著一點強迫和模仿性質,大家都在看原始碼都在分析原始碼,雖然不知道看原始碼能幹什麼,但是就是看就夠了,所以挑選的也是大家都在分析的Volley,所以前面看的幾個框架的原始碼從Volley到最近的OkHttp的原始碼,總感覺是為了看原始碼而看原始碼,雖然最後看完後收穫確實很多,但是感覺方式總是不是特別正確,這次的RecyclerView的分析想法卻不是這樣的,由於對RecyclerView使用的程度和頻率越來越多,越來越多的不解和疑問讓我想要去看一看RecyclerView的原始碼,總的疑惑有下面幾個:
* 1.首先沒有見過特別詳細的RecyclerView的原始碼分析系列,所有關於RecyclerView都是停留在使用或者少數進階使用的部落格

  • 2.RecyclerView,LayoutManager,Adapter,ViewHolder,ItemDecoration這些和RecycleView使用息息相關的類到底是什麼關係

  • 3.RecyclerView作為列表,繪製流程到底什麼樣的

  • 4.RecyclerView有什麼不常用的進階使用方式,但是卻很適合RecyclerView作為很“重”的元件的優化,像setRecyclerPool用處到底是什麼

  • 5.大家都只要要使用RecyclerView替代ListView和GridView,好用,都在用,但是都沒有追究到底這背後的原因到底是什麼,RecyclerView到底比ListView好在哪裡,到底該不該替換,效能

    到底提升多少。

以上問題不一定看完原始碼都能解決(個人能力堪憂啊…),但是帶著問題來看原始碼總會有不錯的收穫,或者大家有什麼關於使用RecyclerView的學習部落格連結,使用經驗和體會都可以在下面評論供大家一起學習討論

思路

當開啟RecyclerVew的原始碼會發現非常的複雜,感覺無從下手,不會像Volley和OkHttp從發起開始到發起結束,目標性那麼明確。那麼既然RecyclerView繼承的是ViewGroup,也就是說RecyclerView其實就是和LinearLayout等佈局一樣的一個自定義ViewGroup。
既然涉及到自定義元件,那麼當我們自己來實現一個自定義ViewGroup,最重要的步驟無非是下面幾點:
1. 重寫onMeasure用於確定自定義ViewGroup的大小
2. 重寫onLayout用於佈局子view的位置
所以原始碼的分析也對應從這裡開始進行分析。

onMeasure

和分析OkHttp的原始碼一樣,這裡就不放上RecyclerView中的onMeasure的所有原始碼了,那樣只會增加我們的理解難度,所以同樣選擇利用虛擬碼進行分析。

protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {
            //layoutManager沒有設定的話,直接走default的方法,所以會為空白
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        if (mLayout.mAutoMeasure) {
            final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                    && heightMode == MeasureSpec.EXACTLY;
            //如果測量是絕對值,則跳過measure過程直接走layout
            if (skipMeasure || mAdapter == null) {
                return;
            }
            if (mState.mLayoutStep == State.STEP_START) {
                //mLayoutStep預設值是 State.STEP_START
                dispatchLayoutStep1();
                //執行完dispatchLayoutStep1()後是State.STEP_LAYOUT
            }
             ..........
            //真正執行LayoutManager繪製的地方
            dispatchLayoutStep2();
            //執行完後是State.STEP_ANIMATIONS
             ..........
            //寬高都不確定的時候,會繪製兩次
            // if RecyclerView has non-exact width and height and if there is at least one child
            // which also has non-exact width & height, we have to re-measure.
            if (mLayout.shouldMeasureTwice()) {
             ..........
                dispatchLayoutStep2();
             ..........            }
        } else {
            if (mHasFixedSize) {
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                return;
            }
             ..........
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
             ..........
            mState.mInPreLayout = false; // clear
        }
    }

原始碼放上了,現在可以一步一步分析onMeasure的過程了,先從第一個小點開始吧。

if (mLayout == null) {
            //layoutManager沒有設定的話,直接走default的方法,所以會為空白
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }

這裡的mLayout其實就是我們給RecyclerView設定的LayoutManager物件,這一段程式碼其實就很好的解釋了為什麼我們有時候初次使用RecyclerView的時候忘記設定LayoutManager後,RecyclerView會沒有按照我們所想的那樣顯示出來。
這裡可以看到,如果mLayout為null的話,會走defaultOnMeasure方法。

void defaultOnMeasure(int widthSpec, int heightSpec) {
        // calling LayoutManager here is not pretty but that API is already public and it is better
        // than creating another method since this is internal.
        final int width = LayoutManager.chooseSize(widthSpec,
                getPaddingLeft() + getPaddingRight(),
                ViewCompat.getMinimumWidth(this));
        final int height = LayoutManager.chooseSize(heightSpec,
                getPaddingTop() + getPaddingBottom(),
                ViewCompat.getMinimumHeight(this));
        setMeasuredDimension(width, height);
    }

可以看到這裡的chooseSize方法其實就是更加寬高的Mode得到相應的值後直接呼叫setMeasuredDimension(width, height)設定寬高了,可以發現這裡其實是沒有進行child的測量就直接return結束了onMeasure過程的,這也就解釋了為什麼我們沒有設定LayoutManager會導致顯示空白了。
接下來技術一個判斷

  if (mLayout.mAutoMeasure) {
}else{
}

這裡其實mAutoMeasure這個值,LinearLayoutManager還是其他兩個Manager,預設值都是true。所以接著往下看。

final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                    && heightMode == MeasureSpec.EXACTLY;
            //如果測量是絕對值,則跳過measure過程直接走layout
            if (skipMeasure || mAdapter == null) {
                return;
            }

這裡解釋的很清楚,如果寬和高的測量值是絕對值時,直接跳過onMeasure方法。那這裡可能有疑問了,如果沒有執行onMeasure方法,那麼子View沒有繪製,會造成空白的情況,但是實際情況是當我們給RecyclerView設定絕對值大小的時候,子View仍可以正常繪製出來。這個問題後面會解答。(onLayout裡會執行子View的繪製)

 if (mState.mLayoutStep == State.STEP_START) {
                //mLayoutStep預設值是 State.STEP_START
                dispatchLayoutStep1();
                //執行完dispatchLayoutStep1()後是State.STEP_LAYOUT
            }

接下來就要開始繪製的準備了,這裡可以看到首先判斷mLayoutStep,這裡mLayoutStep的預設值其實就是 State.STEP_START,並且每次繪製流程結束後,會重置為 State.STEP_START。接下來執行dispatchLayoutStep1();方法dispatchLayoutStep1();其實沒有必要過多分析,因為分析原始碼主要是對於繪製思想的理解,如果過多的糾結於每一行程式碼的含義,那麼會陷入很大的困擾中。這裡就放上官方對於dispatchLayoutStep1();的註釋吧。(順道翻譯一下)

/**
     * 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
     */
    /**
     * 1.處理Adapter的更新
     * 2.決定那些動畫需要執行
     * 3.儲存當前View的資訊
     * 4.如果必要的話,執行上一個Layout的操作並且儲存他的資訊
     */

接下來就是我們的真正執行LayoutManager繪製的地方dispatchLayoutStep2()。

private void dispatchLayoutStep2() {
        ....
        //重寫的getItemCount方法
        mState.mItemCount = mAdapter.getItemCount();
        ....
        // Step 2: Run layout
        mState.mInPreLayout = false;
        mLayout.onLayoutChildren(mRecycler, mState);
         ....
    }

同樣的,這裡放上虛擬碼,便於理解。
這裡注意兩個地方,第一個mAdapter.getItemCount(),可以看到我們每次重寫Adapter時重寫的方法getItemCount方法用到的地方了。
第二個,可以看到mLayout.onLayoutChildren(mRecycler, mState);這個方法,為什麼說RecyclerView將View的繪製交給了LayoutManager,這裡就是最有力的體現,可以看到,這裡將RecycleView內部持有的Recycler和state傳給了LayoutManager的onLayoutChildren方法,單從方法的名字其實就可以看出。這裡我們進入LayoutManager裡看一看。(本次分析給予LinearLayoutManager)

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
        ....
        // resolve layout direction
        //判斷繪製方向,給mShouldReverseLayout賦值,預設是正向繪製,則mShouldReverseLayout是false
        resolveShouldLayoutReverse();
        final View focused = getFocusedChild();
        //mValid的預設值是false,一次測量之後設為true,onLayout完成後會回撥執行reset方法,又變為false
        if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
                || mPendingSavedState != null) {
        ....
            //mStackFromEnd預設是false,除非手動呼叫setStackFromEnd()方法,兩個都會false,異或則為false
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // calculate anchor position and coordinate
            //計算錨點的位置和偏移量
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
        ....
        } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
                >= mOrientationHelper.getEndAfterPadding()
                || mOrientationHelper.getDecoratedEnd(focused)
                <= mOrientationHelper.getStartAfterPadding())) {
         ....
        }
         ....
        //mLayoutFromEnd為false
        if (mAnchorInfo.mLayoutFromEnd) {
            //倒著繪製的話,先往上繪製,再往下繪製
            // fill towards start
            // 從錨點到往上
            updateLayoutStateToFillStart(mAnchorInfo);
            ....
            fill(recycler, mLayoutState, state, false);
            ....
            // 從錨點到往下
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            ....
            //調兩遍fill方法
            fill(recycler, mLayoutState, state, false);
            ....
            if (mLayoutState.mAvailable > 0) {
                // end could not consume all. add more items towards start
            ....
                updateLayoutStateToFillStart(firstElement, startOffset);
                mLayoutState.mExtra = extraForStart;
                fill(recycler, mLayoutState, state, false);
             ....
            }
        } else {
            //正常繪製流程的話,先往下繪製,再往上繪製
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            ....
            fill(recycler, mLayoutState, state, false);
             ....
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            ....
            fill(recycler, mLayoutState, state, false);
             ....
            if (mLayoutState.mAvailable > 0) {
                ....
                // start could not consume all it should. add more items towards end
                updateLayoutStateToFillEnd(lastElement, endOffset);
                 ....
                fill(recycler, mLayoutState, state, false);
                ....
            }
        }
        ....
        layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
        //完成後重置引數
        if (!state.isPreLayout()) {
            mOrientationHelper.onLayoutComplete();
        } else {
            mAnchorInfo.reset();
        }
        mLastStackFromEnd = mStackFromEnd;
    }

這裡雖然已經儘量刪減了很多程式碼,但是還是很多…但是其實原理理解起來還是比較容易的。簡單的說其實可以總結縮略為:

先尋找頁面當前的錨點
以這個錨點未基準,向上和向下分別填充
填充完後,如果還有剩餘的可填充大小,再填充一次

這樣理解起來就比較容易了,接下來我們就一步一步來看一下原始碼。
尋找錨點

        resolveShouldLayoutReverse();
        final View focused = getFocusedChild();
        //mValid的預設值是false,一次測量之後設為true,onLayout完成後會回撥執行reset方法,又變為false
        if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
                || mPendingSavedState != null) {
        ....
            //mStackFromEnd預設是false,除非手動呼叫setStackFromEnd()方法,兩個都會false,異或則為false
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // calculate anchor position and coordinate
            //計算錨點的位置和偏移量
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
        ....
        } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
                >= mOrientationHelper.getEndAfterPadding()
                || mOrientationHelper.getDecoratedEnd(focused)
                <= mOrientationHelper.getStartAfterPadding())) {
         ....
        }

首先執行的resolveShouldLayoutReverse();方法,從方法的命名上可以理解為是否需要倒著繪製

 //判斷繪製方向,給mShouldReverseLayout賦值,預設是正向繪製,則mShouldReverseLayout是false
private void resolveShouldLayoutReverse() {
        // A == B is the same result, but we rather keep it readable
        if (mOrientation == VERTICAL || !isLayoutRTL()) {
            //預設mReverseLayout是false,建構函式,可以通過setReverseLayout來設定
            mShouldReverseLayout = mReverseLayout;
        } else {
            mShouldReverseLayout = !mReverseLayout;
        }
    }

可以看到這裡我註釋寫的很清楚,如果我們沒有手動呼叫setReverseLayout()方法,預設情況下,是不會倒著繪製的。
接下來對於幾個變數的註釋這裡解釋一下。
首先是mAnchorInfo.mValid,這裡mAnchorInfo就是我們要的錨點。mValid的預設值是false,一次測量之後設為true,onLayout完成後會回撥執行reset方法,又變為false
再就是mAnchorInfo.mLayoutFromEnd。

mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;

可以看到這裡用到了位運算子^異或,前面已經分析了,mShouldReverseLayout預設是fasle的,mStackFromEnd預設是false,除非手動呼叫setStackFromEnd()方法,兩個都會false,異或則為false。

 updateAnchorInfoForLayout(recycler, state, mAnchorInfo);

接下來這個方法就是對於錨點位置的確定,裡面其實就是對於當前狀態的偏移量的計算,得出當前的錨點位置,具體比較複雜,這裡就不做分析了。後面就是繪製的地方,主要就是兩種方向,正向(先向上再向下),逆向(先向下再向上),所以這裡我們就看平常的情況。

{
            //正常繪製流程的話,先往下繪製,再往上繪製
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            ....
            fill(recycler, mLayoutState, state, false);
             ....
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            ....
            fill(recycler, mLayoutState, state, false);
             ....
            if (mLayoutState.mAvailable > 0) {
                ....
                // start could not consume all it should. add more items towards end
                updateLayoutStateToFillEnd(lastElement, endOffset);
                 ....
                fill(recycler, mLayoutState, state, false);
                ....
            }

可以看到這裡有兩種方法。
1.updateLayoutStateToFill…()
2.fill()
第一個方法其實就是確定當前方向上錨點的相關的狀態資訊。
這裡最主要的就是第二個方法fill(),可以看到這裡至少呼叫了兩次fill()方法,當還有剩餘可以繪製的時候會再調一次fill()方法。這也證明了我們的想法,是通過錨點分別向上和向下兩次繪製。這裡放上一張圖便於理解
測量流程

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
             RecyclerView.State state, boolean stopOnFocusable) {
            .....
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
                         .....
        return start - layoutState.mAvailable;
    }

這裡fill其實最重要的就是看這裡的layoutChunk(recycler, state, layoutState, layoutChunkResult)方法。

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
                     LayoutState layoutState, LayoutChunkResult result) {
        //next方法很重要
        View view = layoutState.next(recycler);
        if (view == null) {
            if (DEBUG && layoutState.mScrapList == null) {
                throw new RuntimeException("received null view when unexpected");
            }
           ...
        }
        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);
            }
        }
        //測量ChildView
        measureChildWithMargins(view, 0, 0);
        ......
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        //layout Child
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        ......
    }

這個方法其實我是不忍註釋的,滿滿的乾貨啊,首先是我們的next方法,千萬不要因為這個next短小不起眼,就認為不重要。我只能說這個方法很重要

View next(RecyclerView.Recycler recycler) {
            //預設mScrapList=null,但是執行layoutForPredictiveAnimations方法的時候不會為空
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            //重要,從recycler獲得View,mScrapList是被LayoutManager持有,recycler是被RecyclerView持有
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }

第一個mSrapList其實預設是空的,只有執行layoutForPredictiveAnimations前不為空,執行完後又變為空,所以這裡暫時不需要考慮。
可以看到View view = recycler.getViewForPosition(mCurrentPosition)終於看到RecyclerView中快取策略的身影,RecyclerView的快取不會在這篇部落格講解,但是這裡不得不讓我們注意下面從流程上簡單的看一下View view = recycler.getViewForPosition(mCurrentPosition);這個方法

public View getViewForPosition(int position) {
            return getViewForPosition(position, false);
        }
        View getViewForPosition(int position, boolean dryRun) {
            return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
        }
/**
         * Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
         * cache, the RecycledViewPool, or creating it directly.
         */
        /**
         * 註釋寫的很清楚,從Recycler的scrap,cache,RecyclerViewPool,或者直接create建立
         */
        @Nullable
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
          //一堆判斷之後,如果不成立
            holder = mAdapter.createViewHolder(RecyclerView.this, type);            
        }

可以看到這裡,getViewForPosition會呼叫tryGetViewHolderForPositionByDeadline方法,tryGetViewHolderForPositionByDeadline方法的註釋寫的很清楚從Recycler的scrap,cache,RecyclerViewPool,或者直接create建立,這裡我們也看到了我們最熟悉的mAdapter.createViewHolder(RecyclerView.this, type); 方法!
關於RecyclerView的快取策略不出意外應該會在下篇部落格進行分析
next()方法分析結束後,其實就比較容易了。

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);
            }
        }

剩下的就是RecyclerView的addView方法。新增完View後會呼叫

//測量ChildView
        measureChildWithMargins(view, 0, 0);

//----------------------------------------------------------
public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //設定分割線中的回撥方法
            final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
            widthUsed += insets.left + insets.right;
            heightUsed += insets.top + insets.bottom;
            final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
                    getPaddingLeft() + getPaddingRight()
                            + lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
                    canScrollHorizontally());
            final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
                    getPaddingTop() + getPaddingBottom()
                            + lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
                    canScrollVertically());
            if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
                //子View的測量
                child.measure(widthSpec, heightSpec);
            }
        }

從這個方法裡我們看到了子View的測量,當然還有一個需要我們注意的地方那就是mRecyclerView.getItemDecorInsetsForChild(child)

Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (!lp.mInsetsDirty) {
            return lp.mDecorInsets;
        }
        if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
            // changed/invalid items should not be updated until they are rebound.
            return lp.mDecorInsets;
        }
        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
            //getItemOffsets()實現分割線的回撥方法!
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
    }

其實可以看到這裡在測量子View的時候是將我們實現自定義分割線重寫的getItemOffsets方法。這裡其實也就可以理解了自定義分割線的原理就是在子View的測量過程前給上下左右加上自定義分割線所對應設定給這個child的邊距。
測量完成後,緊接著就呼叫了layoutDecoratedWithMargins(view, left, top, right, bottom)對子View完成了layout。
分割線

public void layoutDecoratedWithMargins(View child, int left, int top, int right,
                int bottom) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Rect insets = lp.mDecorInsets;
            //layout
            child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
                    right - insets.right - lp.rightMargin,
                    bottom - insets.bottom - lp.bottomMargin);
        }

終於到此,我們對於onMeasure方法分析結束了,這裡分析完成對於後面的onLayout的分析就比較簡單了。

onLayout

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
      ` ...
        dispatchLayout();
        ...
    }
void dispatchLayout() {
         ....
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
         ...
            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.
         ...
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }

這裡的程式碼就比較好理解了,並且上面提到的問題也就迎刃而解了,當我們給RecyclerView設定固定的寬高的時候,onMeasure是直接跳過了執行,那麼為什麼子View仍然能繪製出來。
這裡可以看到,如果onMeasure沒有執行,mState.mLayoutStep == State.STEP_START就成立,所以仍然會執行 dispatchLayoutStep1(), dispatchLayoutStep2();也就對應的會繪製子View。
而後面的註釋也比較清楚,由於我們在Layout的時候改變了寬高,也會導致dispatchLayoutStep2();,也就是子View的重新繪製。
如果上面情況都沒有,那麼onLayout的作用就僅僅是dispatchLayoutStep3(),而 dispatchLayoutStep3()方法的作用除了重置一些引數,外還和執行動畫有關。

private void dispatchLayoutStep3() {
         //重置引數
        if (mState.mRunSimpleAnimations) {
            // Step 3: Find out where things are now, and process change animations.
            // traverse list in reverse because we may call animateChange in the loop which may
            // remove the target view holder.
            //需要動畫的情況。找出ViewHolder現在的位置,並且處理改變動畫。最後觸發動畫。
            }
            // Step 4: Process view info lists and trigger animations
            mViewInfoStore.process(mViewInfoProcessCallback);
        }
        //成功回撥
    }

到此!!!對於RecyclerView的繪製流程其實我們有了一個大體的瞭解,總結一下關鍵點:

1.RecyclerView是將繪製流程交給LayoutManager處理,如果沒有設定不會測量子View。
2.繪製流程是區分正向繪製和倒置繪製。
3.繪製是先確定錨點,然後向上繪製,向下繪製,fill()至少會執行兩次,如果繪製完還有剩餘空間,則會再執行一次fill()方法。
4.LayoutManager獲得View是從RecyclerView中的Recycler.next()方法獲得,涉及到RecyclerView的快取策略,如果快取沒有拿到,則走我們自己重寫的onCreateView方法。
5.如果RecyclerView寬高沒有寫死,onMeasure就會執行完子View的measure和Layout方法,onLayout僅僅是重置一些引數,如果寫死,子View的measure和layout會延後到onLayout中執行。