RecyclerView源碼解析

引言

在以前的Android的View繪製機制中咱們講過,對於控件的測量以及佈局會經過 onMeasure()onLayout() 方法來實現。因此這裏咱們將這兩個函數做爲入口來研究RecyclerView的整個佈局過程。java

基礎

RecyclerView相對於之前的ListView來講,更加靈活。其所拆分出來的各個類的分工更加明確,很好地體現了咱們常常所說的職責單一原則。咱們這裏先對其中使用到的類進行一下講解緩存

  • LayoutManager:RecyclerView的佈局管理者,主要負責對於RecyclerView子View的測量和佈局工做。
  • RecyclerView.Recycler:緩存的核心類。RecyclerView強大的緩存能力都是基於這個類來實現的。是緩存的核心工具類。
  • Adapter:Adapter的基類。負責將ViewHolder中的數據和RecyclerView中的控件進行綁定處理。
  • ViewHolder:視圖和元數據類。它持有了要顯示的數據信息,包括位置、View、ViewType等。

源碼

不管是View仍是ViewGroup的子類,都是經過 onMeasure() 來實現測量工做的,那麼咱們對於RecyclerView的源碼解析就把onMeasure看成咱們的切入點app

自身測量

//RecyclerView.java
	protected void onMeasure(int widthSpec, int heightSpec) {
        //dispatchLayoutStep1,dispatchLayoutStep2,dispatchLayoutStep3確定會執行,可是會根據具體的狀況來區分是在onMeasure仍是onLayout中執行。
        if (mLayout == null) {//LayoutManager爲空,那麼就使用默認的測量策略
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        if (mLayout.mAutoMeasure) {
            //有LayoutManager,開啓了自動測量
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
            final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            //步驟1 調用LayoutManager的onMeasure方法來進行測量工做
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            //若是width和height都已是精確值,那麼就不用再根據內容進行測量,後面步驟再也不處理
            if (skipMeasure || mAdapter == null) {
                return;
            }
            //若是測量過程後的寬或者高都沒有精確,那麼就須要根據child來進行佈局,從而來肯定其寬和高。
            // 當前的佈局狀態是start
            if (mState.mLayoutStep == State.STEP_START) {
                //佈局的第一部 主要進行一些初始化的工做
                dispatchLayoutStep1();
            }
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
            //執行佈局第二步。先確認子View的大小與佈局
            dispatchLayoutStep2();
            // 佈局過程結束,根據Children中的邊界信息計算並設置RecyclerView長寬的測量值
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
            //檢查是否須要再此測量。若是RecyclerView仍然有非精確的寬和高,或者這裏還有至少一個Child還有非精確的寬和高,咱們就須要再次測量。
            // 好比父子尺寸屬性互相依賴的狀況,要改變參數從新進行一次
            if (mLayout.shouldMeasureTwice()) {
                mLayout.setMeasureSpecs(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
                mState.mIsMeasuring = true;
                dispatchLayoutStep2();
                // now we can get the width and height from the children.
                mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
            }
        } else {
            //有LayoutManager,沒有開啓自動測量。通常系統的三個LayoutManager都是自動測量,
            // 若是是咱們自定義的LayoutManager的話,能夠經過setAutoMeasureEnabled關閉自動測量功能
            //RecyclerView已經設置了固定的Size,直接使用固定值便可
            if (mHasFixedSize) {
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                return;
            }
            //若是在測量過程當中數據發生變化,須要先對數據進行處理
            ...
            // 處理完新更新的數據,而後執行自定義測量操做。
            if (mAdapter != null) {
                mState.mItemCount = mAdapter.getItemCount();
            } else {
                mState.mItemCount = 0;
            }
            eatRequestLayout();
            //沒有設置固定的寬高,則須要進行測量
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            resumeRequestLayout(false);
            mState.mInPreLayout = false;
        }
    }

複製代碼

在這個方法裏面,根據不一樣的狀況進行了不一樣的處理。less

  1. 沒有設置LayoutManager的狀況下,直接使用默認的測量方法。
  2. 當設置了Layoutmanager並且開啓了自動測量功能。
  3. 設置了LayoutManager可是沒有開啓自動測量。

咱們先從最簡單的來分析。ide

第一種:沒有設置LayoutManager。函數

由於RecyclerView的全部的測量和佈局工做都是交給LayoutManager來處理的,若是沒有設置的話,只能使用默認的測量方案了。工具

第三種:有LayoutManager,並且關閉了自動測量功能。佈局

關閉測量的狀況下不須要考慮子View的大小和佈局。直接按照正常的流程來進行測量便可。若是直接已經設置了固定的寬高,那麼直接使用固定值便可。若是沒有設置固定寬高,那麼就按照正常的控件同樣,根據父級的要求與自身的屬性進行測量。post

第二種:有LayoutManager,開啓了自動測量。性能

這種狀況是最複雜的,須要根據子View的佈局來調整自身的大小。須要知道子View的大小和佈局。因此RecyclerView將佈局的過程提早到這裏來進行了。

咱們簡化一下代碼再看

//RecyclerView.java
if (mLayout.mAutoMeasure) {
    		//調用LayoutManager的onMeasure方法來進行測量工做
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            //若是width和height都已是精確值,那麼就不用再根據內容進行測量,後面步驟再也不處理
            if (skipMeasure || mAdapter == null) {
                return;
            }
            if (mState.mLayoutStep == State.STEP_START) {
                //佈局的第一部 主要進行一些初始化的工做
                dispatchLayoutStep1();
            }
    		...
            //開啓了自動測量,須要先確認子View的大小與佈局
            dispatchLayoutStep2();
            ...
            //再根據子View的狀況決定自身的大小
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

            if (mLayout.shouldMeasureTwice()) {
                ...
                //若是有父子尺寸屬性互相依賴的狀況,要改變參數從新進行一次
                dispatchLayoutStep2();
                mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
            }
        }
複製代碼

對於RecyclerView的測量和繪製工做,是須要 dispatchLayoutStep1 , dispatchLayoutStep2 , dispatchLayoutStep3 這三步來執行的,step1裏是進行預佈局,主要跟記錄數據更新時須要進行的動畫所需的信息有關,step2就是實際循環執行了子View的測量佈局的一步,而step3主要是用來實際執行動畫。並且經過 mLayoutStep 記錄了當前執行到了哪一步。在開啓自動測量的狀況下若是沒有設置固定寬高,那麼會執行setp1和step2。在step2執行完後就能夠調用 setMeasuredDimensionFromChildren 方法,根據子類的測量佈局結果來設置自身的大小。

咱們先不進行分析step1,step2和step3的具體功能。直接把 onLayout 的代碼也貼出來,看一下這3步是如何保證都可以執行的。

//RecyclerView.java
	@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        dispatchLayout();
    }
    
    void dispatchLayout() {
        if (mAdapter == null) {//沒有設置adapter,返回
            Log.e(TAG, "No adapter attached; skipping layout");
            // leave the state in START
            return;
        }
        if (mLayout == null) {//沒有設置LayoutManager,返回
            Log.e(TAG, "No layout manager attached; skipping layout");
            // leave the state in START
            return;
        }
        mState.mIsMeasuring = false;
        //在onMeasure階段,若是寬高是固定的,那麼mLayoutStep == State.STEP_START 並且dispatchLayoutStep1和dispatchLayoutStep2不會調用
        //因此這裏就會調用一下
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()|| mLayout.getHeight() != getHeight()) {
            //在onMeasure階段,若是執行了dispatchLayoutStep1,可是沒有執行dispatchLayoutStep2,就會執行dispatchLayoutStep2
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            mLayout.setExactMeasureSpecsFrom(this);
        }
        //最終調用dispatchLayoutStep3
        dispatchLayoutStep3();
    }
複製代碼

能夠看到,其實在 onLayout 階段會根據 onMeasure 階段3個步驟執行到了哪一個,而後會在 onLayout 中把剩下的步驟執行。

OK,到如今整個流程通了,在這3個步驟中,step2就是執行了子View的測量佈局的一步,也是最重要的一環,因此咱們將關注的重點放在這個函數。

//RecyclerView.java
	private void dispatchLayoutStep2() {
        //禁止佈局請求
        eatRequestLayout();
        ...
        mState.mInPreLayout = false;
        //調用LayoutManager的layoutChildren方法來佈局
        mLayout.onLayoutChildren(mRecycler, mState);
		...
        resumeRequestLayout(false);
    }
複製代碼

這裏調用LayoutManager的 onLayoutChildren 方法,將對於子View的測量和佈局工做交給了LayoutManager。並且咱們在自定義LayoutManager的時候也必需要重寫這個方法來描述咱們的佈局錯略。這裏咱們分析最常用的 LinearLayoutManager(後面簡稱LLM) 。咱們這裏只研究垂直方向佈局的狀況。

//LinearLayoutManager.java
	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
複製代碼

在方法的開始位置,直接就扔給了咱們一段說明文檔,告訴了咱們 LinearLayoutManager 中的佈局策略。簡單翻譯一下:

  1. 經過子控件和其餘的變量信息。找到一個錨點和錨點項的位置。
  2. 從錨點的位置開始,往上,填充佈局子View,直到填滿區域
  3. 從錨點的位置開始,往下,填充佈局子View,直到填滿區域
  4. 滾動以知足需求,如堆棧從底部

這裏有個比較關鍵的詞,就是 錨點(AnchorInfo) ,其實 LLM 的佈局並非從上往下一個個進行的。而是極可能從整個佈局的中間某個點開始的,而後朝一個方向一個個填充,填滿可見區域後,朝另外一個方向進行填充。至於先朝哪一個方向填充,是根據具體的變量來肯定的。

錨點的選擇

AnchorInfo 類須要可以有效的描述一個具體的位置信息,咱們首先類內部的幾個重要的成員變量。

//LinearLayoutManager.java
	//簡單的數據類來保存錨點信息
    class AnchorInfo {
        //錨點參考View在整個數據中的position信息,即它是第幾個View
        int mPosition;
        //錨點的具體座標信息,填充子View的起始座標。當positon=0的時候,若是隻有一半View可見,那麼這個數據可能爲負數
        int mCoordinate;
        //是否從底部開始佈局
        boolean mLayoutFromEnd;
        //是否有效
        boolean mValid;
複製代碼

能夠看到,經過 AnchorInfo 就能夠準確的定位當前的位置信息了。那麼在LLM中,這個錨點的位置是如何肯定的呢?

咱們從源碼中去尋找答案。

//LinearLayoutManager.java
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        ...
        //確認LayoutState存在
        ensureLayoutState();
        //禁止回收
        mLayoutState.mRecycle = false;
        //計算是否須要顛倒繪製。是從底部到頂部繪製,仍是從頂部到底部繪製(在LLM的構造函數中,其實能夠設置反向繪製)
        resolveShouldLayoutReverse();
        //若是當前錨點信息非法,滑動到的位置不可用或者有須要恢復的存儲的SaveState
        if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION || mPendingSavedState != null) {
            //重置錨點信息
            mAnchorInfo.reset();
            //是否從end開始進行佈局。由於mShouldReverseLayout和mStackFromEnd默認都是false,那麼咱們這裏能夠考慮按照默認的狀況來進行分析,也就是mLayoutFromEnd也是false
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            //計算錨點的位置和座標
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            //設置錨點有效
            mAnchorInfo.mValid = true;
        }
複製代碼

在須要肯定錨點的時候,會先將錨點進行初始化,而後經過 updateAnchorInfoForLayout 方法來肯定錨點的信息。

//LinearLayoutManager.java
	private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
        //從掛起的數據更新錨點信息 這個方法通常不會調用到
        if (updateAnchorFromPendingData(state, anchorInfo)) {
            return;
        }
        //**重點方法 從子View來肯定錨點信息(這裏會嘗試從有焦點的子View或者列表第一個位置的View或者最後一個位置的View來肯定)
        if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
            return;
        }
        //進入這裏說明如今都沒有肯定錨點(好比設置Data後尚未繪製View的狀況下),就直接設置RecyclerView的頂部或者底部位置爲錨點(按照默認狀況,這裏的mPosition=0)。
        anchorInfo.assignCoordinateFromPadding();
        anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
    }
複製代碼

錨點的肯定方案主要有3個:

  1. 從掛起的數據獲取錨點信息。通常不會執行。
  2. 從子View來肯定錨點信息。好比說notifyDataSetChanged方法的時候,屏幕上原來是有View的,那麼就會經過這種方式獲取
  3. 若是上面兩種方法都沒法肯定,則直接使用0位置的View做爲錨點參考position。

最後一種何時會發生呢?其實就是沒有子View讓咱們做爲參考。好比說第一次加載數據的時候,RecyclerView一片空白。這時候確定沒有任何子View可以讓咱們做爲參考。

那麼當有子View的時候,咱們經過 updateAnchorFromChildren 方法來肯定錨點位置。

//LinearLayoutManager.java
	//從現有子View中肯定錨定。大多數狀況下,是起始或者末尾的有效子View(通常是未移除,展現在咱們面前的View)。
    private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
        //沒有數據,直接返回false
        if (getChildCount() == 0) {
            return false;
        }
        final View focused = getFocusedChild();
        //優先選取得到焦點的子View做爲錨點
        if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {
            //保持獲取焦點的子view的位置信息
            anchorInfo.assignFromViewAndKeepVisibleRect(focused);
            return true;
        }
        if (mLastStackFromEnd != mStackFromEnd) {
            return false;
        }
        //根據錨點的設置信息,從底部或者頂部獲取子View信息
        View referenceChild = anchorInfo.mLayoutFromEnd ? findReferenceChildClosestToEnd(recycler, state) : findReferenceChildClosestToStart(recycler, state);
        if (referenceChild != null) {
            anchorInfo.assignFromView(referenceChild);
            ...
            return true;
        }
        return false;
    }
複製代碼

經過子View肯定錨點座標也是進行了3種狀況的處理

  1. 沒有數據,直接返回獲取失敗
  2. 若是某個子View持有焦點,那麼直接把持有焦點的子View做爲錨點參考點
  3. 沒有子View持有焦點,通常會選擇最上(或者最下面)的子View做爲錨點參考點

通常狀況下,都會使用第三種方案來肯定錨點,因此咱們這裏也主要關注一下這裏的方法。按照咱們默認的變量信息,這裏會經過 findReferenceChildClosestToStart 方法獲取可見區域中的第一個子View做爲錨點的參考View。而後調用 assignFromView 方法來肯定錨點的幾個屬性值。

//LinearLayoutManager.java
		public void assignFromView(View child) {
            if (mLayoutFromEnd) {
                //若是是從底部佈局,那麼獲取child的底部的位置設置爲錨點
                mCoordinate = mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper.getTotalSpaceChange();
            } else {
                //若是是從頂部開始佈局,那麼獲取child的頂部的位置設置爲錨點座標(這裏要考慮ItemDecorator的狀況)
                mCoordinate = mOrientationHelper.getDecoratedStart(child);
            }
            //mPosition賦值爲參考View的position
            mPosition = getPosition(child);
        }
複製代碼

mPostion這個變量很好理解,就是子View的位置值,那麼 mCoordinate 是個什麼鬼?咱們 getDecoratedStart 是怎麼處理的就知道了。

//LinearLayoutManager.java
        //建立mOrientationHelper。咱們按照垂直佈局來進行分析
        if (mOrientationHelper == null) {
            mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation);
        }
        //OrientationHelper.java
    public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
        return new OrientationHelper(layoutManager) {
            @Override
            @Override
            public int getDecoratedStart(View view) {
                final RecyclerView.LayoutParams params =  (RecyclerView.LayoutParams)view.getLayoutParams();
                //
                return mLayoutManager.getDecoratedTop(view) - params.topMargin;
            }

複製代碼

比較難理麼,咱們上個簡陋的圖解釋一下

image-20200417130157946

能夠看到在使用子控件進行錨點信息確認時,通常會選擇屏幕中可見的子View的position爲錨點。這裏會選取屏幕上第一個可見View,也就是positon=1的子View做爲參考點, mCoordinate 被賦值爲1號子View上面的Decor的頂部位置。

佈局的填充

回到主線 onLayoutChildren 函數。當咱們的錨點信息確認之後,剩下的就是從這個位置開始進行佈局的填充。

if (mAnchorInfo.mLayoutFromEnd) {//從end開始佈局
            //倒着繪製的話,先從錨點往上,繪製完再從錨點往下
            //設置繪製方向信息爲從錨點往上
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtra = extraForStart;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
            final int firstElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForEnd += mLayoutState.mAvailable;
            }
            //設置繪製方向信息爲從錨點往下
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                extraForStart = mLayoutState.mAvailable;
                updateLayoutStateToFillStart(firstElement, startOffset);
                mLayoutState.mExtra = extraForStart;
                fill(recycler, mLayoutState, state, false);
                startOffset = mLayoutState.mOffset;
            }
        } else {//從起始位置開始佈局
            // 更新layoutState,設置佈局方向朝下
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;
            //開始填充佈局
            fill(recycler, mLayoutState, state, false);
            //結束偏移
            endOffset = mLayoutState.mOffset;
            //繪製後的最後一個view的position
            final int lastElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForStart += mLayoutState.mAvailable;
            }
            //更新layoutState,設置佈局方向朝上
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtra = extraForStart;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            //再次填充佈局
            fill(recycler, mLayoutState, state, false);
            //起始位置的偏移
            startOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                extraForEnd = mLayoutState.mAvailable;
                updateLayoutStateToFillEnd(lastElement, endOffset);
                mLayoutState.mExtra = extraForEnd;
                fill(recycler, mLayoutState, state, false);
                endOffset = mLayoutState.mOffset;
            }
        }
複製代碼

能夠看到,根據不一樣的繪製方向,這裏面作了不一樣的處理,只是填充的方向相反而已,具體的步驟是類似的。都是從錨點開始往一個方向進行View的填充,填充滿之後再朝另外一個方向填充。填充子View使用的是 fill() 方法。

由於對於繪製方向都按照默認的來處理,因此這裏咱們看看分析else的代碼,並且第一次填充是朝下填充。

//在LinearLayoutManager中,進行界面重繪和進行滑動兩種狀況下,往屏幕上填充子View的工做都是調用fill()進行
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
        //可用區域的像素數
        final int start = layoutState.mAvailable;
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            //將滑出屏幕的View回收掉
            recycleByLayoutState(recycler, layoutState);
        }
        //剩餘繪製空間=可用區域+擴展空間。
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        //循環佈局直到沒有剩餘空間了或者沒有剩餘數據了
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            //初始化layoutChunkResult
            layoutChunkResult.resetInternal();
            //**重點方法 添加一個child,而後將繪製的相關信息保存到layoutChunkResult
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (layoutChunkResult.mFinished) {//若是佈局結束了(沒有view了),退出循環
                break;
            }
            //根據所添加的child消費的高度更新layoutState的偏移量。mLayoutDirection爲+1或者-1,經過乘法來處理是從底部往上佈局,仍是從上往底部開始佈局
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                //消費剩餘可用空間
                remainingSpace -= layoutChunkResult.mConsumed;
            }
            ...
        }
        //返回本次佈局所填充的區域
        return start - layoutState.mAvailable;
    }
複製代碼

fill 方法中,會判斷當前的是否還有剩餘區域能夠進行子View的填充。若是沒有剩餘區域或者沒有子View,那麼就返回。不然就經過 layoutChunk 來進行填充工做,填充完畢之後更新當前的可用區域,而後依次遍歷循環,直到不知足條件爲止。

循環中的填充是經過 layoutChunk 來實現的。

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
        //經過緩存獲取當前position所須要展現的ViewHolder的View
        View view = layoutState.next(recycler);
        if (view == null) {
            //若是咱們將視圖放置在廢棄視圖中,這可能會返回null,這意味着沒有更多的項須要佈局。
            result.mFinished = true;
            return;
        }
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            //根據方向調用addView方法添加子View
            if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
                //這裏是即將消失的View,可是須要設置對應的移除動畫
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        //調用measure測量view。這裏會考慮到父類的padding
        measureChildWithMargins(view, 0, 0);
        //將本次子View消費的區域設置爲子view的高(或者寬)
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        //找到view的四個邊角位置
        int left, top, right, bottom;
        ...
        //調用child.layout方法進行佈局(這裏會考慮到view的ItemDecorator等信息)
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        //若是視圖未被刪除或更改,則使用可用空間
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.isFocusable();
    }
複製代碼

這裏主要作了5個處理

  1. 經過 layoutState 獲取要展現的View
  2. 經過 addView 方法將子View添加到佈局中
  3. 調用 measureChildWithMargins 方法測量子View
  4. 調用 layoutDecoratedWithMargins 方法佈局子View
  5. 根據處理的結果,填充LayoutChunkResult的相關信息,以便返回以後,可以進行數據的計算。

若是隻是考慮第一次數據加載,那麼到目前爲止,咱們的整個頁面經過兩次 fill 就可以將整個屏幕填充完畢了。

複用機制

對於RecyclerView的複用機制,咱們就不得不提 RecyclerView.Recycler 類了。它的職責就是負責對於View的回收以及複用工做。Recycler 依次經過 Scrap、CacheView、ViewCacheExtension還有RecycledViewPool 四級緩存機制實現對於RecyclerView的快速繪製工做。

Scrap

Scrap是RecyclerView中最輕量的緩存,它不參與滑動時的回收複用,只是做爲從新佈局時的一種臨時緩存。它的目的是,緩存當界面從新佈局的先後都出如今屏幕上的ViewHolder,以此省去沒必要要的從新加載與綁定工做。

在RecyclerView從新佈局的時候(不包括RecyclerView初始的那次佈局,由於初始化的時候屏幕上原本並無任何View),先調用**detachAndScrapAttachedViews()將全部當前屏幕上正在顯示的View以ViewHolder爲單位標記並記錄在列表中,在以後的fill()**填充屏幕過程當中,會優先從Scrap列表裏面尋找對應的ViewHolder填充。從Scrap中直接返回的ViewHolder內容沒有任何的變化,不會進行從新建立和綁定的過程。

Scrap列表存在於Recycler模塊中。

public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;
        ...
}
複製代碼

能夠看到,Scrap實際上包括了兩個ViewHolder類型的ArrayList。mAttachedScrap負責保存將會原封不動的ViewHolder,而mChangedScrap負責保存位置會發生移動的ViewHolder,注意只是位置發生移動,內容仍舊是原封不動的。

img

上圖描述的是咱們在一個RecyclerView中刪除B項,而且調用了**notifyItemRemoved()**時,mAttachedScrap與mChangedScrap分別會臨時存儲的View狀況。此時,A是在刪除先後徹底沒有變化的,它會被臨時放入mAttachedScrap。B是咱們要刪除的,它也會被放進mAttachedScrap,可是會被額外標記REMOVED,而且在以後會被移除。C和D在刪除B後會向上移動位置,所以他們會被臨時放入mChangedScrap中。E在這次操做前並無出如今屏幕中,它不屬於Scrap須要管轄的,Scrap只會緩存屏幕上已經加載出來的ViewHolder。在刪除時,A,B,C,D都會進入Scrap,而在刪除後,A,C,D都會回來,其中C,D只進行了位置上的移動,其內容沒有發生變化。

RecyclerView的局部刷新,依賴的就是Scrap的臨時緩存,咱們須要經過notifyItemRemoved()、notifyItemChanged()等系列方法通知RecyclerView哪些位置發生了變化,這樣RecyclerView就能在處理這些變化的時候,使用Scrap來緩存其它內容沒有發生變化的ViewHolder,因而就完成了局部刷新。須要注意的是,若是咱們使用**notifyDataSetChanged()**方法來通知RecyclerView進行更新,其會標記全部屏幕上的View爲FLAG_INVALID,從而不會嘗試使用Scrap來緩存一下子還會回來的ViewHolder,而是通通直接扔進RecycledViewPool池子裏,回來的時候就要從新走一遍綁定的過程。

Scrap只是做爲佈局時的臨時緩存,它和滑動時的緩存沒有任何關係,它的detach和從新attach只臨時存在於佈局的過程當中。佈局結束時Scrap列表應該是空的,其成員要麼被從新佈局出來,要麼將被移除,總之在佈局過程結束的時候,兩個Scrap列表中都不該該再存在任何東西。

CacheView是什麼?

CacheView是一個以ViewHolder爲單位,負責在RecyclerView列表位置產生變更的時候,對剛剛移出屏幕的View進行回收複用的緩存列表。

public final class Recycler {
        ...
        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
        int mViewCacheMax = DEFAULT_CACHE_SIZE;
        ...
}
複製代碼

咱們能夠看到,在Recycler中,mCachedView與前面的Scrap同樣,也是一個以ViewHolder爲單位存儲的ArrayList。這意味着,它也是對於ViewHolder整個進行緩存,在複用時不須要通過建立和綁定過程,內容不發生改變。並且它有個最大緩存個數限制,默認狀況下是2個。

img

CacheView.png

從上圖中能夠看出,CacheView將會緩存剛變爲不可見的View。這個緩存工做的進行,是發生在fill()調用時的,因爲佈局更新和滑動時都會調用fill()來進行填充,所以這個場景在滑動過程當中會反覆出現,在佈局更新時也可能由於位置變更而出現。fill()幾經週轉最終會調用recycleViewHolderInternal(),裏面將會出現mCachedViews.add()。上面提到,CacheView有最大緩存個數限制,那麼若是超過了緩存會怎樣呢?

void recycleViewHolderInternal(ViewHolder holder) {
            ...
            if (forceRecycle || holder.isRecyclable()) {
                if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                                | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE)) {
                    // Retire oldest cached view 回收並替換最早緩存的那個
                    int cachedViewSize = mCachedViews.size();
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }
                    mCachedViews.add(targetCacheIndex, holder);
                }
               ...
        }
複製代碼

在**recycleViewHolderInternal()中有這麼一段,若是Recycler發現緩存進來一個新ViewHolder時,會超過最大限制,那麼它將先調用recycleCachedViewAt(0)將最早緩存進來的那個ViewHolder回收進RecycledViewPool池子裏,而後再調用mCachedViews.add()**添加新的緩存。也就是說,咱們在滑動RecyclerView的時候,Recycler會不斷地緩存剛剛滑過變成不可見的View進入CacheView,在達到CacheView的上限時,又會不斷地替換CacheView裏的ViewHolder,將它們扔進RecycledViewPool裏。若是咱們一直朝同一個方向滑動,CacheView其實並無在效率上產生幫助,它只是不斷地在把後面滑過的ViewHolder進行了緩存;而若是咱們常常上下來回滑動,那麼CacheView中的緩存就會獲得很好的利用,畢竟複用CacheView中的緩存的時候,不須要通過建立和綁定的消耗。

RecycledViewPool

前面提到,在Srap和CacheView不肯意緩存的時候,都會丟進RecycledViewPool進行回收,所以RecycledViewPool能夠說是Recycler中的一個終極回收站。

public static class RecycledViewPool {
        private SparseArray<ArrayList<ViewHolder>> mScrap =
                new SparseArray<ArrayList<ViewHolder>>();
        private SparseIntArray mMaxScrap = new SparseIntArray();
        private int mAttachCount = 0;

        private static final int DEFAULT_MAX_SCRAP = 5;
複製代碼

咱們能夠在RecyclerView中找到RecycledViewPool,能夠看見它的保存形式是和上述的Srap、CacheView都不一樣的,它是以一個SparseArray嵌套一個ArrayList對ViewHolder進行保存的。緣由是RecycledViewPool保存的是以ViewHolder的viewType爲區分(咱們在重寫RecyclerView的**onCreateViewHolder()**時能夠發現這裏有個viewType參數,能夠藉助它來實現展現不一樣類型的列表項)的多個列表。

與前二者不一樣,RecycledViewPool在進行回收的時候,目標只是回收一個該viewType的ViewHolder對象,並無保存下原來ViewHolder的內容,在複用時,將會調用bindViewHolder() 按照咱們在**onBindViewHolder()**描述的綁定步驟進行從新綁定,從而搖身一變變成了一個新的列表項展現出來。

一樣,RecycledViewPool也有一個最大數量限制,默認狀況下是5。在沒有超過最大數量限制的狀況下,Recycler會盡可能把將被廢棄的ViewHolder回收到RecycledViewPool中,以期能被複用。值得一提的是,RecycledViewPool只會按照ViewType進行區分,只要ViewType是相同的,甚至能夠在多個RecyclerView中進行通用的複用,只要爲它們設置同一個RecycledViewPool就能夠了。

總的來看,RecyclerView着重在兩個場景使用緩存與回收複用進行了性能上的優化。一是,在數據更新時,利用Scrap實現局部更新,儘量地減小沒有被更改的View進行無用地從新建立與綁定工做。二是,在快速滑動的時候,重複利用已經滑過的ViewHolder對象,以儘量減小從新建立ViewHolder對象時帶來的壓力。整體的思路就是:只要沒有改變,就直接重用;只要能不建立或從新綁定,就儘量地偷懶。

滑動處理

在研究滑動前,咱們先對 LayoutState 這個類的幾個變量作一下說明。

  • mOffset:佈局起始位置的偏移量(應該是錨點裏面設置的mCoordinate)
  • mAvailable:在佈局方向上的能夠填充的像素值,也就是空閒出來的區域
  • mScrollingOffset:不建立新視圖的狀況下進行滾動的距離。 好比說我某個View上半部分顯示了一半,那麼這時候我往上滑動一半距離的話之內,是不須要建立新的子View的。這個mScrollingOffset就是我在不建立視圖的前提下能夠滑動的最大距離。

滑動後的數據處理

當滾動發生時,會觸發 scrollHorizontallyBy 方法

//LinearLayoutManager.java
		public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,RecyclerView.State state) {
        if (mOrientation == VERTICAL) {
            return 0;
        }
        return scrollBy(dx, recycler, state);
    }

    int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || dy == 0) {
            return 0;
        }
        //標記正在滾動
        mLayoutState.mRecycle = true;
        ensureLayoutState();
        //確認滾動方向
        final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        final int absDy = Math.abs(dy);
        //更新layoutState,會更新其展現的屏幕區域,偏移量等。好比說當往上滑動的時候,底部會有dy距離的空白區域,這時候,須要調用fill來填充這個dy距離的區域
        updateLayoutState(layoutDirection, absDy, true, state);
        //調用fill進行填充展現在客戶面前的view
        final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
        ...
        //記錄本次滾動的距離
        mLayoutState.mLastScrollDelta = scrolled;
        return scrolled;
    }
複製代碼

當滾動時主要進行了兩個處理

  1. 經過 updateLayoutState 方法更新layoutState內部的相關屬性的。
  2. 調用 fill 進行數據的填充。

咱們爲了更好的理解layoutState內部的屬性關係,簡單看一下 updateLayoutState 內部的實現。

//LinearLayoutaManager.java
	private void updateLayoutState(int layoutDirection, int requiredSpace,boolean canUseExistingSpace, RecyclerView.State state) {
        int scrollingOffset;
        if (layoutDirection == LayoutState.LAYOUT_END) {
            //獲取當前顯示的最底部的View
            final View child = getChildClosestToEnd();
            //設置當前顯示的子View的底部的偏移量(包括了Decor的高度)
            mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
            //底部錨點位置減去RecyclerView的高度的話,剩下的就是咱們滾動scrollingOffset之內,不會繪製新的View
            //getEndAfterPadding=RecyclerView的高度-padding的高度
            scrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding();
        } 
        ....
複製代碼

方法裏面的註釋已經很詳細了。

有了複用基礎和對這幾個變量的理解以後,咱們從新回到 fill 中,去理解LLM是如何進行緩存處理的。

View的回收

首先看一下View的回收。

//LinearLayoutManager.java
	int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
            //重點方法 ** 將滑出屏幕的View回收掉
            recycleByLayoutState(recycler, layoutState);
        }

    private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        if (!layoutState.mRecycle || layoutState.mInfinite) {
            return;
        }
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            //從End端開始回收視圖
            recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
        } else {
            //從Start端開始回收視圖
            recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
        }
    }
複製代碼

這裏咱們考慮手指上滑的狀況,也就是 recycleViewsFromStart 。另外一種狀況是類似的,能夠本身去理解

//從頭部回收View
    private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
        //limit表示滑動多少之內不會繪製
        final int limit = dt;
        //返回附加到父視圖的當前子View的數量
        final int childCount = getChildCount();
        ...
            //遍歷子View
            for (int i = 0; i < childCount; i++) {
                //獲取到子View
                View child = getChildAt(i);
                //若是當前的View的底部位置>limit,那麼也就是會有View須要繪製,頂部的View也就須要回收了
                //這裏有個邏輯,就是若是底部的View不須要繪製,那麼頂部的View就不會進行回收
                if (mOrientationHelper.getDecoratedEnd(child) > limit || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                    recycleChildren(recycler, 0, i);
                    return;
                }
            }
        }
    }

複製代碼

通過跟蹤最後會進入到

//LinearLayoutManager.java
	private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
          ...
                removeAndRecycleViewAt(i, recycler);
           ...
        }
    }
    
    //RecyclerView.java
    public void removeAndRecycleViewAt(int index, Recycler recycler) {
    	final View view = getChildAt(index);
    	removeViewAt(index);
    	recycler.recycleView(view);
    }
    
    //RecyclerView.java
        public void recycleView(View view) {
            recycleViewHolderInternal(holder);
        }
複製代碼

最終的回收操做會經過 recycleViewHolderInternal 方法來執行。

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)) {
                    // Retire oldest cached view
                    //滑動的視圖,先保存在mCachedViews中
                    int cachedViewSize = mCachedViews.size();
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        //mCachedViews只能緩存mViewCacheMax個,那麼須要將最久的那個移到RecycledViewPool
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize;
                    if (ALLOW_THREAD_GAP_WORK && cachedViewSize > 0  && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                        // when adding the view, skip past most recently prefetched views
                        int cacheIndex = cachedViewSize - 1;
                        while (cacheIndex >= 0) {
                            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                            if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                                break;
                            }
                            cacheIndex--;
                        }
                        targetCacheIndex = cacheIndex + 1;
                    }
                    //將本次回收的ViewHolder放到mCachedViews中
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                if (!cached) {//若是已經緩存了。那麼此處不會執行了。
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            }
            ...
            mViewInfoStore.removeViewHolder(holder);
            if (!cached && !recycled && transientStatePreventsRecycling) {
                holder.mOwnerRecyclerView = null;
            }
        }
複製代碼

這部分代碼屬於在滑動時的View回收的過程

  1. 若是不知足可以回收的條件,則直接拋出異常
  2. 若是知足回收條件,會看 cachedViewSize 是否已滿,若是滿了,就移除最先的那個到緩存池 RecycledViewPool ,而後將當前須要回收的放入到 cachedViewSize 中。若是沒滿,則直接放入了。
  3. 存放線程池 RecycledViewPool 會按照ViewType來緩存到不一樣的隊列,每一個類型的隊列最多緩存5個。若是已經滿了,則再也不緩存。

View的複用

fill 填充方法中,不只包含了對於滑出屏幕的View的回收處理,還會將即將展現的界面經過複用ViewHolder來達到快速處理的效果。而對於複用的調用則是在 layoutChunk 中的 layoutState.next(recycler) 來觸發的。

//LinearLayoutManager.java
        void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
            //經過緩存獲取當前position所須要展現的ViewHolder的View
            View view = layoutState.next(recycler);
		//LinearLayoutManager.java 
        View next(RecyclerView.Recycler recycler) {
            ...
            final View view = recycler.getViewForPosition(mCurrentPosition);
            ...
        }

		//RecyclerView.java
        public View getViewForPosition(int position) {
            return getViewForPosition(position, false);
        }
		//RecyclerView.java
        View getViewForPosition(int position, boolean dryRun) {
            return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
        }
複製代碼

最終對於ViewHolder的複用邏輯是由 tryGetViewHolderForPositionByDeadline 來處理的。

//RecyclerView.java
	ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            //嘗試從mChangedScrap中獲取。當數據位置發生變化的時候,會走這個邏輯。不如說notifyItemRemove()後,下面的數據會上移,會走這個邏輯
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            if (holder == null) {
                //根據position依次嘗試從mAttachedScrap、隱藏的列表、一級緩存(mCachedViews)中獲取
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if (holder != null) {
                    //檢驗獲取的holder是否合法。不合法,就會將holder進行回收。若是合法,則標記fromScrapOrHiddenOrCache爲true。代表holder是從這緩存中獲取的。
                    if (!validateViewHolderForOffsetPosition(holder)) {
                        ...
                        holder = null;
                    } else {
                        fromScrapOrHiddenOrCache = true;
                    }
                }
            }
            if (holder == null) {
                ...
                if (mAdapter.hasStableIds()) {
                    //根據id依次嘗試從mAttachedScrap、一級緩存(mCachedViews)中獲取
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);
                    if (holder != null) {
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
                }
                //嘗試從咱們自定義的mViewCacheExtension(二級緩存)中去獲取
                if (holder == null && mViewCacheExtension != null) {
                    final View view = mViewCacheExtension .getViewForPositionAndType(this, position, type);
                    if (view != null) {
                        holder = getChildViewHolder(view);
                        if (holder == null) {
                            throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view which does not have a ViewHolder");
                        } else if (holder.shouldIgnore()) {
                            throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view that is ignored. You must call stopIgnoring before" + " returning this view.");
                        }
                    }
                }
                if (holder == null) {
                    //從緩存池裏面獲取
                    holder = getRecycledViewPool().getRecycledView(type);
                    if (holder != null) {
                        holder.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
                            invalidateDisplayListInt(holder);
                        }
                    }
                }
                if (holder == null) {
                    long start = getNanoTime();
                    if (deadlineNs != FOREVER_NS&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                        return null;
                    }
                    //若是仍然沒法獲取的話,調用Adatper的createViewHolder方法建立一個ViewHolder
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    if (ALLOW_THREAD_GAP_WORK) {
                        // only bother finding nested RV if prefetching
                        RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                        if (innerView != null) {
                            holder.mNestedRecyclerView = new WeakReference<>(innerView);
                        }
                    }

                    long end = getNanoTime();
                    mRecyclerPool.factorInCreateTime(type, end - start);
                }
            }
			...
            boolean bound = false;
            if (mState.isPreLayout() && holder.isBound()) {
                // do not update unless we absolutely have to.
                //數據不須要綁定(通常從mChangedScrap,mAttachedScrap中獲得的緩存Holder是不須要進行從新綁定的)
                holder.mPreLayoutPosition = position;
            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                //holder沒有綁定數據,或者須要更新或者holder無效,則須要從新進行數據的綁定
                if (DEBUG && holder.isRemoved()) {
                    throw new IllegalStateException("Removed holder should be bound and it should"+ " come here only in pre-layout. Holder: " + holder);
                }
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                //這裏會進行數據的綁定
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }
			...
            return holder;
        }
複製代碼

這一段的邏輯就是咱們的ViewHolder的整個複用的流程。能夠彙總一下

  1. mChangedScrap中獲取去獲取
  2. 根據position依次嘗試從mAttachedScrap隱藏的列表、**一級緩存(mCachedViews)**中獲取
  3. 根據id依次嘗試從mAttachedScrap、**一級緩存(mCachedViews)**中獲取
  4. 嘗試從咱們自定義的**mViewCacheExtension(二級緩存)**中去獲取
  5. 根據ViewType從緩存池裏面獲取
  6. 若是上面都沒法獲取的話就經過adapter的 createViewHolder 來建立ViewHolder

當咱們獲取到ViewHolder之後會須要進行綁定,也就是將咱們的數據展現在View中。若是獲取的ViewHolder緩存已經進行了數據綁定的話,則無需再進行處理,不然就須要經過 tryBindViewHolderByDeadline 方法調用 adapter的bindViewHolder 來進行數據的綁定。

總結

整篇文章到這裏結束了,相對來講內容仍是比較多的。先從RecyclerView的測量看成入口,在測量的過程當中,提到了複用機制。最後經過RecyclerView對滑動的處理方法,從源碼層面講解了Holder的回收和複用的實現機制。

彙總一下本次源碼解析學到的新知識:

  1. 在RecyclerView中,內部類Recycler主要負責回收和複用工做。
  2. 當滑動RecyclerView時,是不斷的調用 fill 去判斷是否須要填充的。
  3. LinearLayoutManager能夠經過setReverseLayout設置反向遍歷佈局。第一項放置在UI的末尾,第二項放置在它以前。
  4. 當View滑動回收的時候,會回調adapter的 onViewDetachedFromWindow 方法
  5. 深刻理解了RecyclerView的緩存機制和原理
  6. 對於RecyclerView的子View的佈局過程和原理進行了講解

本文由 開了肯 發佈!

同步公衆號[開了肯]

image-20200404120045271
相關文章
相關標籤/搜索