RecyclerView預(yù)布局

RecyclerView應(yīng)該是我們使用非常頻繁的一個(gè)組件 我們也有必要學(xué)習(xí)分析一下RecyclerView#onLayout工作流程 對(duì)我們?nèi)蘸蠓治鰞?yōu)化RecyclerView也會(huì)有幫助
美其名曰 知其然也知所以然

預(yù)布局是什么?

這個(gè)問(wèn)題其實(shí)我們可以先閱讀完下面的代碼 然后再來(lái)回想這個(gè)問(wèn)題 所以我先給出結(jié)論
首先預(yù)布局就是先布局一次(??湊個(gè)字?jǐn)?shù)) 然后會(huì)形成一個(gè)快照(pre-layout) 如item1234 然后再布局一次(post-layout) 形成另外一張快照 item134 這樣我們其實(shí)就知道了整個(gè)動(dòng)畫軌跡 就可以生成動(dòng)畫
上面看不懂 沒(méi)關(guān)系 看下圖??

pre-layout.png

下面分別是三種狀態(tài)

  • 初始狀態(tài)
  • pre-layout(預(yù)布局階段 生成一個(gè)快照 詳細(xì)代碼我們會(huì)在下面分析)
  • post-layout(布局階段 生成另一個(gè)快照)

然后我們就知道了item3的初始位置和終止位置 就可以生成動(dòng)畫并執(zhí)行

源碼調(diào)試手段

因?yàn)槲覀兎治鲈创a的過(guò)程中 經(jīng)常有很多個(gè)分支 不知道如何是好?? 所以就需要斷點(diǎn)調(diào)試手段了
首先我們啟動(dòng)一個(gè)AVD模擬器 然后AVD的版本號(hào)一定要和compileSdkVersion版本號(hào)是一樣的 不然會(huì)出現(xiàn)代碼行數(shù)不一致導(dǎo)致調(diào)試不了問(wèn)題
然后我們就可以愉快的找一個(gè)調(diào)試點(diǎn)開(kāi)始調(diào)試?yán)?/p>

RecyclerView#onLayout流程

先從onLayout開(kāi)始閱讀代碼(主要是我找不到閱讀入口 就先隨便找一個(gè)??)

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

調(diào)用了dispatchLayout()方法 繼續(xù)看一下

void dispatchLayout() {
        if (mAdapter == null) {
            Log.e(TAG, "No adapter attached; skipping layout");
            // leave the state in START
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "No layout manager attached; skipping layout");
            // leave the state in START
            return;
        }
        mState.mIsMeasuring = false;
        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 {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }

我們發(fā)現(xiàn)會(huì)按照調(diào)用順序
dispatchLayoutStep1()-> dispatchLayoutStep2()-> dispatchLayoutStep3()

我們挨個(gè)分析一下各方法功能 然后最后會(huì)做一個(gè)總結(jié) 朋友們也可以先點(diǎn)擊??先看一眼結(jié)論

開(kāi)始分析吧 任重而道遠(yuǎn)

 private void dispatchLayoutStep1() {
         ......
         //設(shè)置mInPreLayout 預(yù)布局標(biāo)志位
        mState.mInPreLayout = mState.mRunPredictiveAnimations;
               mState.mLayoutStep = State.STEP_LAYOUT;
        ......
        //調(diào)用LayoutManager.onLayoutChildren方法
        mLayout.onLayoutChildren(mRecycler, mState);
        ......
    }

我們看到這里設(shè)置了mInPreLayout 我們草率的先認(rèn)定它是預(yù)布局的標(biāo)志位 當(dāng)然結(jié)果也是對(duì)的??
全局搜索一下mInPreLayout的賦值 發(fā)現(xiàn)只有這里兩處對(duì)mInPreLayout賦值
一處是onMeasure()時(shí)

 if (mState.mRunPredictiveAnimations) {
     mState.mInPreLayout = true;
 } 

那么mState.mRunPredictiveAnimations一定是true 不然就沒(méi)辦法判斷了

image.png

我們斷點(diǎn)驗(yàn)證一下 發(fā)現(xiàn)mState.mRunPredictiveAnimations確實(shí)是true

LinearLayoutManager#onLayoutChildren()

我們這里以LinearLayoutManager來(lái)分析 其他幾個(gè)LayoutManger其實(shí)也差不太多
根據(jù)注釋 我們看到onLayoutChildren主要做了四件事情

  • 檢查children 找到第一個(gè)需要變化(刪除或者添加)的position
  • 從底向上填充布局
  • 從上向下填充布局
  • 滾動(dòng)布局來(lái)填充
@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
        .......
        //將所有view先回收
        detachAndScrapAttachedViews(recycler);
        final int firstLayoutDirection;
        //從下往上
        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()方法 我們可能在很多文章都看到過(guò)這個(gè)方法 會(huì)依次調(diào)用layoutChunk來(lái)對(duì)布局進(jìn)行填充 這邊我們需要重點(diǎn)關(guān)注一下預(yù)布局相關(guān)的不同點(diǎn)?? 畢竟我們這篇文章的主題是分析預(yù)布局的啊

我們看一下fill()方法 這邊我們會(huì)重點(diǎn)關(guān)注一下remainSpace 我們之前一直在提的 預(yù)布局會(huì)形成一張快照 postLayout也會(huì)形成另一張快照 fill()方法中會(huì)對(duì)兩次布局做不同的處理

    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;
        ......
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;//可用剩余布局空間
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        //只要remainingSpace>0 并且position<itemsCount 
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            //layout item
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (layoutChunkResult.mFinished) {
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            //這里我們重點(diǎn)看一下 只有Post-Layut 或者mIgnoreConsumed為false remainingSpace才會(huì)減少 ①
            //否則remainingSpace不會(huì)減少
            if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                    || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                remainingSpace -= layoutChunkResult.mConsumed;
            }

            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);
            }
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }
        return start - layoutState.mAvailable;
    }

我們將上面的代碼精簡(jiǎn)了一下 并且寫了一些注釋 我們發(fā)現(xiàn)
他是一個(gè)while循環(huán) 直到remainingSpace < 0 或者 position > itemsCount 才會(huì)布局

我們上面提了好幾次的 Pre-Layout 會(huì)形成一張1234的快照 這張快照包含即將要?jiǎng)h除的item2 以及刪除item2之后填充的item4
這里就會(huì)產(chǎn)生一個(gè)疑問(wèn) 他是如何計(jì)算Pre-Layout的高度呢?

我們看到最開(kāi)始代碼int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
??哈哈哈哈哈哈哈哈 結(jié)果出來(lái)了 就是layoutState.mExtraFillSpace 然而現(xiàn)實(shí)給我好好的上了一課?? 調(diào)試發(fā)現(xiàn)上面的變量為0

答案:其實(shí)是上面注釋①的地方 只有Pre-Layout或者mIgnoreConsumedfalse remainingSpace才會(huì)減少

我搜了一下mIgnoreConsumed什么時(shí)候會(huì)為false

 // Consume the available space if the view is not removed OR changed
    if (params.isItemRemoved() || params.isItemChanged()) {
        result.mIgnoreConsumed = true;
    }

只有當(dāng)需要remvoe 或者需要需要change 才會(huì)ignore

所以 朋友們 ??結(jié)果很明顯了啊 預(yù)布局的時(shí)候 remainingSpace會(huì)忽略需要remove的item 所以會(huì)形成一張1234的快照

dispatchLayoutStep1()終于分析完了 分析源碼的過(guò)程總是枯燥又帶著一點(diǎn)收獲啊

dispatchLayoutStep1()小總結(jié)

dispatchLayoutStep1()也就是我們今天需要分析的Pre-Layout的流程 生成了一張即將消失的內(nèi)容+即將顯示內(nèi)容的快照

然后我們接著分析一下dispatchLayoustStep2() 也就是Post-Layout

分析一下 Post-Layout??

我們先看一眼dispatchLayoutStep2的源碼

private void dispatchLayoutStep2() {
        // Step 2: Run layout
        //將mInPreLayout置為false
        //這里表示預(yù)布局階段已經(jīng)正式結(jié)束了
        mState.mInPreLayout = false;
        //又到了熟悉的onLayoutChildren()環(huán)節(jié)
        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();
        stopInterceptRequestLayout(false);
    }

上面的源碼比較簡(jiǎn)短 我們發(fā)現(xiàn)dispatchLayoutStep2()方法中 首先mState.mInPreLayout = false; 表示Pre-Layout已經(jīng)結(jié)束了 然后進(jìn)入了我們熟悉的onLayoutChildren環(huán)節(jié)
我們上面剛剛分析過(guò)onLayoutChildren 因?yàn)?strong>state.isPreLayout()一定為false 所以remainingSpace一定會(huì)扣除所有空間
所以 結(jié)論出現(xiàn)了! dispatchLayoutStep2會(huì)生成一張item134的快照

等等! 我寫到這里的時(shí)候 突然引發(fā)了我一個(gè)另一個(gè)疑惑 上面所說(shuō)的并不能證明dispatchLayoutStep2會(huì)生成一張item134的快照?只能說(shuō)明會(huì)生成一張item1234?
咋回事啊? 然后我又去徹底翻了一遍源碼 終于找到了根源所在 :-(

我們之前沒(méi)有分析layoutChunk方法 這個(gè)方法在fill()中 會(huì)從recycler中獲取合適的ViewHolderView 然后添加到RecyclerView

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
     //根據(jù)當(dāng)前position 獲取View
    View view = layoutState.next(recycler);
    ......
} 

View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    //根據(jù)position獲取View
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

這里會(huì)根據(jù)mCurrentPosition獲取對(duì)應(yīng)的ViewHolder 然后將View添加到RecyclerView 但是我們還是沒(méi)辦法得到 為什么Post-Layout會(huì)生成item134呢?

Post-LayoutPre-Layout 還有兩個(gè)地方有區(qū)別

public int getItemCount() {
    return mInPreLayout
            ? (mPreviousLayoutItemCount - mDeletedInvisibleItemCountSincePreviousLayout)
            : mItemCount;
}

我們發(fā)現(xiàn)預(yù)布局和后布局的ItemCount是不一樣的
另外就是我們?cè)?code>fill()之前 會(huì)先回收所有的ViewHolder 想看scrapView代碼可以直接點(diǎn)擊跳轉(zhuǎn)看一下
scrapView 會(huì)將所有的ViewHolder放入mAttachedScrap
mAttachedScrap中存在position分別為0、0、1、2的四個(gè)表項(xiàng) 如下圖(被Remove的ViewHolderposition會(huì)置為0)

image.png

根據(jù)上面兩個(gè)條件 我們會(huì)發(fā)現(xiàn) 在fill()的過(guò)程中 只會(huì)生成item134快照
終于分析出來(lái)為啥是item134了 凌晨一點(diǎn)還在碼字 腦子轉(zhuǎn)的太累了??

如果你還想問(wèn) 那什么時(shí)候把刪除的position置為0的? 我只能猜測(cè)是(不能保證)processAdapterUpdatesAndSetAnimationFlags(); 然后就只能靠你自己分析了 我已經(jīng)分析不動(dòng)了??

<span id = "dispatch方法">小總結(jié)</span>

1. onLayoutChildren()

  • dispatchLayoutStep1()
    預(yù)布局階段 生成布局快照(remainingSpace忽略即將remove的item)
  • dispatchLayoutStep2()
    post-layout階段 生成布局快照(不帶即將remove的item)
  • dispatchLayoutStep3()
    執(zhí)行動(dòng)畫 因?yàn)樯厦鎯蓚€(gè)步驟已經(jīng)生成了開(kāi)始狀態(tài)和結(jié)束狀態(tài)的快照 所以我們可以拿到動(dòng)畫的起始位置和終止位置

思考另外一個(gè)問(wèn)題,RecyclerView刪除動(dòng)畫是如何執(zhí)行的?

我們上面已經(jīng)分析了dispatchLayoutStep1()dispatchLayoutStep2 已經(jīng)得到了兩次快照所有item的位置信息 接下來(lái)就可以開(kāi)始執(zhí)行動(dòng)畫了 執(zhí)行動(dòng)畫的方法是dispatchLayoutStep3() 代碼流程等我睡醒再分析一下吧?? 太困了

<span id = "scrap"> detachAndScrapAttachedViews ()</span>

我們看到onLayoutChildren的時(shí)候 會(huì)先調(diào)用detachAndScrapAttachedViews 將所有View detach 然后再fill() 我們分析一下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()) {
        //如果isInvalid() 就調(diào)用removeViewAt
        removeViewAt(index);
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
         //detachView
        detachViewAt(index);
        recycler.scrapView(view);
        mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
    }
}

這里又不知道會(huì)走哪個(gè)分支了?? 不過(guò) Debug是我們的好伙伴啊?? 我們斷點(diǎn)發(fā)現(xiàn) 是走下面分支的 我們繼續(xù)往下分析??(分析源碼就是這樣 點(diǎn)到為止)

上面的detachViewAt(index); 主要是將view從children中雙向刪除 會(huì)調(diào)用到ViewGroupremoveFromArray 不是我們這里的重點(diǎn)吶 我們先跳過(guò) 我們分析一下recycler.scrapView(view);

然后會(huì)將所有的View回收到mAttachedScrap中

void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        //如果holder沒(méi)有失效 沒(méi)有被移除 則放入mAttachedScrap中
        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 {
         //只有當(dāng)holder需要改變才會(huì)放入mChangedScrap中
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}

總結(jié)

能堅(jiān)持看完的都是勇士 分析一篇源碼也真的是很累啊 不過(guò)還是受益良多 分析完預(yù)布局源碼 現(xiàn)在對(duì)RecyclerView的布局流程認(rèn)知更加清晰了 對(duì)以后優(yōu)化和分析ReyclerView也會(huì)很有幫助 如果有對(duì)RecyclerView復(fù)用機(jī)制感興趣的朋友 可以閱讀我另外一篇文章啊 鏈接貼在下面啦

RecyclerView緩存回收源碼分析

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容