抽絲剝繭RecyclerView - 化整為零

前言

抽絲剝繭RecyclerView系列文章的目的在于幫助Android開發(fā)者提高對RecyclerView的認(rèn)知,本文是整個系列的第一章。

RecyclerView已經(jīng)出來很久了,很多開發(fā)者對于RecyclerView的使用早已信手拈來。如下就是一張使用網(wǎng)格布局的RecyclerView:

RecyclerView

不過,對于RecyclerView這種明星控件的了解僅僅停留在使用的程度,顯然是不能夠讓我們成為高級工程師的。如果你看過RecyclerView包中的源碼,那你應(yīng)該和我的心情一樣復(fù)雜,光一個RecyclerView.class文件的源碼就多達(dá)13000行。

對于源碼閱讀方式,我很贊成郭神在Glide源碼分析中所說:

抽絲剝繭、點(diǎn)到即止。抽絲剝繭、點(diǎn)到即止。應(yīng)該認(rèn)準(zhǔn)一個功能點(diǎn),然后去分析這個功能點(diǎn)是如何實(shí)現(xiàn)的。但只要去追尋主體的實(shí)現(xiàn)邏輯即可,千萬不要試圖去搞懂每一行代碼都是什么意思,那樣很容易會陷入到思維黑洞當(dāng)中,而且越陷越深。

所以,我在閱讀RecyclerView源碼的時候先確定好自己想好了解的功能點(diǎn):

  1. 數(shù)據(jù)轉(zhuǎn)化為具體的子視圖。
  2. 視圖回收利用方式。
  3. 布局多樣性原因。
  4. 布局動畫多樣性原因。

閱讀姿勢:我選擇了版本為25.3.1RecyclerView,不知道什么原因,我點(diǎn)進(jìn)28.0.0版本的RecyclerView庫中查看RecyclerView.class代碼時,雖然類縮短至7000行,但是注釋沒了以及其他的問題,我不得不使用其他版本的RecyclerView庫。

想要深入原理,沒有什么是一遍調(diào)試解決不了的,如果有,那就是調(diào)試第二遍。

目錄

目錄

一、RecyclerView使用和介紹

LinearLayoutManager為例,我們看一下RecyclerView的使用方式:

RecyclerView mRecyclerView = findViewById(R.id.recycle);
// 設(shè)置布局方式
mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
// 適配器,MainAdapter繼承自RecyclerView.Adapter<VH extends RecyclerView.ViewHolder>
MainAdapter mAdapter = new MainAdapter();
mRecyclerView.setAdapter(mAdapter);
// 添加分割線的方法
// mRecyclerView.addItemDecoration();
// 設(shè)置布局動畫的方法,可以自定義
// mRecyclerView.setItemAnimator();

以及RecyclerView各個部分的作用:

主要的類 作用
LayoutManager 負(fù)責(zé)RecyclerViewView的布局,常用的有LinearLayoutManager(線性布局),還有GridLayoutManager(網(wǎng)格布局)和StaggeredGridLayoutManager(瀑布布局)等。
Adapter 負(fù)責(zé)將數(shù)據(jù)轉(zhuǎn)變成視圖,使用時需要繼承該類。
ItemAnimator 子視圖動畫,RecyclerView有默認(rèn)的子視圖動畫,也可自定義實(shí)現(xiàn)。
ItemDecoration 分隔線,需自定義實(shí)現(xiàn)。

以上是我們使用RecyclerView的時候能夠直觀看到的部分,還有一個很重要但是不直接使用的類:

主要的類 作用
Recycler 負(fù)責(zé)ViewHolder的回收和提供。

二、源碼分析

1. RecyclerView三大工作流程

RecyclerView的源碼那么多,我們先按照使用時的路線進(jìn)行分析。

1.1 構(gòu)造函數(shù)

通常,我們會在布局文件中使用RecyclerView,所以我們的入口就變成了:

public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
}

public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    // ... 省略一些實(shí)例的初始化
    
    if (attrs != null) {
        int defStyleRes = 0;
        TypedArray a = context.obtainStyledAttributes(attrs, styleable.RecyclerView, defStyle, defStyleRes);
        String layoutManagerName = a.getString(styleable.RecyclerView_layoutManager);
        // ... 這里唯一值得關(guān)注就是看布局文件是否指定LayoutManager
        a.recycle();
        this.createLayoutManager(context, layoutManagerName, attrs, defStyle, defStyleRes);
       // ...
    } else {
        // ...
    }
    // ...
}

由于我們可以在RecyclerView的布局文件中使用app:layoutManager指定LayoutManager,如果指定了具體的LayoutManager,最終會在上面的RecyclerView#createLayoutManager方法中利用反射生成一個具體的LayoutManager實(shí)例。

1.2 設(shè)置LayoutManager和Adapter

研究自定義View的時候,最快的研究方法就是直接查看onMeasureonLayoutonDraw三大方法,研究RecyclerView也是如此。

上面我們說到了布局文件,之后,我們會在Activity或者其他地方獲取RecyclerView,再往下,我們會為RecyclerView設(shè)置LayoutManager(如未在布局文件中設(shè)置的情況下)、Adapter以及可能使用的ItemDecoration,這些方法都會調(diào)用RecyclerView#requestLayout方法,從而刷新RecyclerView。

先從RecyclerView#setLayoutManager講起:

public void setLayoutManager(@Nullable RecyclerView.LayoutManager layout) {
    if (layout != this.mLayout) {
        // 停止?jié)L動
        this.stopScroll();
        if (this.mLayout != null) {
            // 因?yàn)槭堑谝淮卧O(shè)置,所以mLayout為空
            // ... 代碼省略 主要是對之前的LayoutManager 進(jìn)行移除前的操作
        } else {
            this.mRecycler.clear();
        }
        this.mChildHelper.removeAllViewsUnfiltered();
        this.mLayout = layout;
        if (layout != null) {
            // 對新的LayoutManager進(jìn)行設(shè)置
            this.mLayout.setRecyclerView(this);
            if (this.mIsAttached) {
                this.mLayout.dispatchAttachedToWindow(this);
            }
        }
        this.mRecycler.updateViewCacheSize();
        // 重點(diǎn) 通知界面重新布局和重繪
        this.requestLayout();
    }
}

RecyclerView#requestLayout會刷新布局,所以該跳到ViewGroup繪制的相關(guān)方法了?不,因?yàn)?code>RecyclView中的Adapter為空,Adapter為空,就沒有數(shù)據(jù),那看一個空視圖還有什么意思呢?So,我們還需要看設(shè)置適配器的RecyclerView#setAdapter方法:

public void setAdapter(@Nullable RecyclerView.Adapter adapter) {
    // 凍結(jié)當(dāng)前布局,不讓進(jìn)行子布局的更新
    this.setLayoutFrozen(false);
    // 重點(diǎn)關(guān)注的方法
    this.setAdapterInternal(adapter, false, true);
    this.processDataSetCompletelyChanged(false);
    // 再次請求布局的重新繪制
    this.requestLayout();
}

繼續(xù)深入查看RecyclerView#setAdapterInternal方法:

private void setAdapterInternal(@Nullable RecyclerView.Adapter adapter, Boolean compatibleWithPrevious, Boolean removeAndRecycleViews) {
    if (this.mAdapter != null) {
        // 第一次進(jìn)入mAdapter為null,故不會進(jìn)入該代碼塊
        // 主要是對舊的mAdapter的數(shù)據(jù)監(jiān)聽器解除注冊
        this.mAdapter.unregisterAdapterDataObserver(this.mObserver);
        this.mAdapter.onDetachedFromRecyclerView(this);
    }
    if (!compatibleWithPrevious || removeAndRecycleViews) {
        // 更換適配器的時候移除所有的子View
        this.removeAndRecycleViews();
    }
    this.mAdapterHelper.reset();
    RecyclerView.Adapter oldAdapter = this.mAdapter;
    this.mAdapter = adapter;
    if (adapter != null) {
        // 新的適配器注冊數(shù)據(jù)監(jiān)聽器
        adapter.registerAdapterDataObserver(this.mObserver);
        adapter.onAttachedToRecyclerView(this);
    }
    if (this.mLayout != null) {
        this.mLayout.onAdapterChanged(oldAdapter, this.mAdapter);
    }
    this.mRecycler.onAdapterChanged(oldAdapter, this.mAdapter, compatibleWithPrevious);
    this.mState.mStructureChanged = true;
}

可以看出,上面的代碼主要是針對Adapter發(fā)生變化的情況下做出的一些修改,RecyclerView.AdapterDataObserver是數(shù)據(jù)變化接口,當(dāng)適配器中的數(shù)據(jù)發(fā)生增刪改的時候最終會調(diào)用該接口的實(shí)現(xiàn)類,從該接口的命名以及注冊操作和取消注冊操作可以看出其使用的是觀察者模式。LayoutManagerAdapter設(shè)置完成以后就可以直奔主題了。

1.3 onMeasure

View工作流程的第一步:

protected void onMeasure(int widthSpec, int heightSpec) {
    if (this.mLayout == null) {
        this.defaultOnMeasure(widthSpec, heightSpec);
    } else {
        // LinearLayoutManager#isAutoMeasureEnabled為True
        // GridLayoutManager繼承子LinearLayoutManager isAutoMeasureEnabled同樣為true
        // 這種情況下,我們主要分析this.mLayout.isAutoMeasureEnabled()為true的場景下
        if (!this.mLayout.isAutoMeasureEnabled()) {
            // ... 省略
        } else {
            int widthMode = MeasureSpec.getMode(widthSpec);
            int heightMode = MeasureSpec.getMode(heightSpec);
            // ... 測量 最后還是走ViewGroup測量子布局的那套
            this.mLayout.onMeasure(this.mRecycler, this.mState, widthSpec, heightSpec);
            Boolean measureSpecModeIsExactly = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            // 如果當(dāng)前的RecyclerView的布局方式是設(shè)置了具體高寬或Match_Parent或mAdapter為null就直接返回
            if (measureSpecModeIsExactly || this.mAdapter == null) {
                return;
            }
            if (this.mState.mLayoutStep == State.STEP_START) {
                this.dispatchLayoutStep1();
            }
            this.mLayout.setMeasureSpecs(widthSpec, heightSpec);
            this.mState.mIsMeasuring = true;
            this.dispatchLayoutStep2();
            this.mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
            if (this.mLayout.shouldMeasureTwice()) {
                this.mLayout.setMeasureSpecs(MeasureSpec.makeMeasureSpec(this.getMeasuredWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(this.getMeasuredHeight(), MeasureSpec.EXACTLY));
                this.mState.mIsMeasuring = true;
                this.dispatchLayoutStep2();
                this.mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
            }
        }
    }
}

顯然,從上面的代碼我們可以得出結(jié)論:measureSpecModeIsExactlytrue或者Adapter為空,我們會提前結(jié)束onMeasure的測量過程。

如果看過View的工作流程的同學(xué)應(yīng)該對SpecMode很熟悉,什么情況下SpecMode會為EXACITY呢?以RecyclerView為例,通常情況下,如果RecyclerView的寬為具體數(shù)值或者Match_Parent的時候,那么它的SpecMode很大程度就為EXACITYmeasureSpecModeIsExactlytrue需要保證高和寬的SpecMode都為EXACITY,當(dāng)然,ViewSpecMode還與父布局有關(guān),不了解的的同學(xué)可以查閱一下相關(guān)的資料。

如果你的代碼中的RecyclerView沒有使用Wrap_Content,那么大部分使用場景中的RecyclerView長寬的SpecMode都為EXACITY,我這么說,不是意味著我要拋棄return下方的關(guān)鍵方法RecyclerView#dispatchLayoutStep1RecyclerView#dispatchLayoutStep2,因?yàn)樗鼈冊诹硪粋€工作流程onLayout中也會執(zhí)行,所以我們放到onLayout中講解。

1.4 onLayout

View工作流程的第二步:

protected void onLayout(Boolean changed, int l, int t, int r, int b) {
    TraceCompat.beginSection("RV OnLayout");
    this.dispatchLayout();
    TraceCompat.endSection();
    this.mFirstLayoutComplete = true;
}

void dispatchLayout() {
    if (this.mAdapter == null) {
        // ...
    } else if (this.mLayout == null) {
        // ...
    } else {
        this.mState.mIsMeasuring = false;
        // 根據(jù)當(dāng)前State的不同執(zhí)行不同的流程
        if (this.mState.mLayoutStep == STEP_START) {
            this.dispatchLayoutStep1();
            this.mLayout.setExactMeasureSpecsFrom(this);
            this.dispatchLayoutStep2();
        } else if (!this.mAdapterHelper.hasUpdates() && this.mLayout.getWidth() == this.getWidth() && this.mLayout.getHeight() == this.getHeight()) {
            this.mLayout.setExactMeasureSpecsFrom(this);
        } else {
            this.mLayout.setExactMeasureSpecsFrom(this);
            this.dispatchLayoutStep2();
        }
        this.dispatchLayoutStep3();
    }
}

mState實(shí)例初始化中,mState.mLayoutStep默認(rèn)為STEP_STARTRecyclerView#dispatchLayoutStep1方法肯定是要進(jìn)入的:

private void dispatchLayoutStep1() {
    // 全部清空位置信息
    mViewInfoStore.clear();
    // 確定mState.mRunSimpleAnimations和mState.mRunPredictiveAnimations
    // ...
    // 預(yù)布局狀態(tài)跟mState.mRunPredictiveAnimations相關(guān)
    mState.mInPreLayout = mState.mRunPredictiveAnimations;
    // ...
    if (mState.mRunSimpleAnimations) {
        // Step 0: Find out where all non-removed items are, pre-layout
        int count = mChildHelper.getChildCount();
        for (int i = 0; i < count; ++i) {
            // ...
            // 存儲子View的位置信息...
            mViewInfoStore.addToPreLayout(holder, animationInfo);
        }
    }
    if (mState.mRunPredictiveAnimations) {
        // 其實(shí)我也不太理解PreLayout布局的意義,放出來看看
        // Step 1: run prelayout: This will use the old positions of items. The layout manager
        // is expected to layout everything, even removed items (though not to add removed
        // items back to the container). This gives the pre-layout position of APPEARING views
        // which come into existence as part of the real layout.
      
        // 真實(shí)布局之前嘗試布局一次
        // temporarily disable flag because we are asking for previous layout
        mLayout.onLayoutChildren(mRecycler, mState);
        for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
            //...
            if (!mViewInfoStore.isInPreLayout(viewHolder)) {
                // ...
                if (wasHidden) {
                    recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
                } else {
                    mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
                }
            }
        }
        // we don't process disappearing list because they may re-appear in post layout pass.
        clearOldPositions();
    } else {
        clearOldPositions();
    }
    // 
    mState.mLayoutStep = State.STEP_LAYOUT;
}

private void processAdapterUpdatesAndSetAnimationFlags() {
    // ...
    // mFirstLayoutComplete 會在RecyclerView第一次完成onLayout變?yōu)門rue
    Boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
    mState.mRunSimpleAnimations = mFirstLayoutComplete
                    && mItemAnimator != null
                    && (mDataSetHasChangedAfterLayout
                    || animationTypeSupported
                    || mLayout.mRequestedSimpleAnimations)
                    && (!mDataSetHasChangedAfterLayout
                    || mAdapter.hasStableIds());
    mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
                    && animationTypeSupported
                    && !mDataSetHasChangedAfterLayout
                    && predictiveItemAnimationsEnabled();
}

我們需要關(guān)注mState.mRunSimpleAnimationsmState.mRunPredictiveAnimations為true時機(jī),從代碼上來看,這兩個屬性為true必須存在mItemAnimator,是否意味著子View動畫的執(zhí)行者mItemAnimator,另外,mViewInfoStore.addToPreLayout(holder, animationInfo);也得關(guān)注,ViewInfoStoreRecyclerView記錄了ViewHolder中子View的位置信息和狀態(tài)。

再看RecyclerView#dispatchLayoutStep2方法:

private void dispatchLayoutStep2() {
    // ...
    // 預(yù)布局結(jié)束 進(jìn)入真實(shí)的布局過程
    this.mState.mInPreLayout = false;
    // 實(shí)際的布局交給了LayoutManager
    this.mLayout.onLayoutChildren(this.mRecycler, this.mState);
    // ...
    // 是否有動畫
    this.mState.mRunSimpleAnimations = this.mState.mRunSimpleAnimations && this.mItemAnimator != null;
    // 變更狀態(tài) 準(zhǔn)備播放動畫 STEP_ANIMATIONS-4
    this.mState.mLayoutStep = State.STEP_ANIMATIONS;
    // ...
}

RecyclerView#dispatchLayoutStep2方法中我們可以看到,RecyclerView自身沒有實(shí)現(xiàn)給子View布局,而是將布局方式交給了LayoutManagerLayoutManager的深入研究我會在之后的博客和大家討論。

打鐵趁熱,我們查看RecyclerView#dispatchLayoutStep3,代碼較多,精簡后如下:

private void dispatchLayoutStep3() {
    this.mState.assertLayoutStep(State.STEP_ANIMATIONS);
    // ... 省略
    this.mState.mLayoutStep = State.STEP_START;
    if (this.mState.mRunSimpleAnimations) {
        for (int i = this.mChildHelper.getChildCount() - 1; i >= 0; --i) {
            // ...省略
            // 總結(jié)下來就是兩個步驟:
            // 1.添加真實(shí)的布局信息
            this.mViewInfoStore.addToPostLayout(holder, animationInfo);
        }
        // 2.挨個執(zhí)行動畫
        this.mViewInfoStore.process(this.mViewInfoProcessCallback);
    }
    //... 清空信息
    this.mViewInfoStore.clear();
}

調(diào)用執(zhí)行動畫函數(shù)ViewInfoStore#process的時候,可以看到放入?yún)?shù)mViewInfoProcessCallback,從名字可以看出,這是一個回調(diào)的接口,所以,我猜動畫的真實(shí)的執(zhí)行應(yīng)該在實(shí)現(xiàn)接口的方法中實(shí)現(xiàn),不過,我們還是要先看ViewInfoStore中的動畫如何執(zhí)行:

void process(ProcessCallback callback) {
    for (int index = mLayoutHolderMap.size() - 1; index >= 0; index --) {
        final ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
        final InfoRecord record = mLayoutHolderMap.removeAt(index);
        if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
            // Appeared then disappeared. Not useful for animations.
            callback.unused(viewHolder);
        } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
            // Set as "disappeared" by the LayoutManager (addDisappearingView)
            if (record.preInfo == null) {
                // similar to appear disappear but happened between different layout passes.
                // this can happen when the layout manager is using auto-measure
                callback.unused(viewHolder);
            } else {
                callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);
            }
        } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
            // Appeared in the layout but not in the adapter (e.g. entered the viewport)
            callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
        } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
            // Persistent in both passes. Animate persistence
            callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
        } else if ((record.flags & FLAG_PRE) != 0) {
            // Was in pre-layout, never been added to post layout
            callback.processDisappeared(viewHolder, record.preInfo, null);
        } else if ((record.flags & FLAG_POST) != 0) {
            // Was not in pre-layout, been added to post layout
            callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
        } else if ((record.flags & FLAG_APPEAR) != 0) {
            // Scrap view. RecyclerView will handle removing/recycling this.
        } else if (DEBUG) {
            throw new IllegalStateException("record without any reasonable flag combination:/");
        }
        // 釋放record
        InfoRecord.recycle(record);
    }
}

// 回調(diào)的接口
interface ProcessCallback {
    void processDisappeared(ViewHolder var1, @NonNull ItemHolderInfo var2, @Nullable ItemHolderInfo var3);
    void processAppeared(ViewHolder var1, @Nullable ItemHolderInfo var2, ItemHolderInfo var3);
    void processPersistent(ViewHolder var1, @NonNull ItemHolderInfo var2, @NonNull ItemHolderInfo var3);
    void unused(ViewHolder var1);
}

之前存儲的和ViewHolder位置狀態(tài)相關(guān)InfoRecord被一個個取出,然后將ViewHolderInfoRecord交給ProcessCallback,如我們所料,ViewInfoStore#process只是對ViewHolder進(jìn)行分類,具體的實(shí)現(xiàn)還是在RecyclerView中的回調(diào),最后查看一下具體實(shí)現(xiàn):

this.mViewInfoProcessCallback = new ProcessCallback() {
    // ... 這里我們只展示一個方法就行了
    public void processAppeared(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preInfo, RecyclerView.ItemAnimator.ItemHolderInfo info) {
        RecyclerView.this.animateAppearance(viewHolder, preInfo, info);
    }
    // ...
};

void animateAppearance(@NonNull RecyclerView.ViewHolder itemHolder, @Nullable RecyclerView.ItemAnimator.ItemHolderInfo preLayoutInfo, @NonNull RecyclerView.ItemAnimator.ItemHolderInfo postLayoutInfo) {
    itemHolder.setIsRecyclable(false);
    if (this.mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) {
        this.postAnimationRunner();
    }
}

限于篇幅,這里我只展示了ProcessCallback中實(shí)現(xiàn)的一個方法processAppeared,在該方法中,它調(diào)用了RecyclerView#animateAppearance方法,動畫的任務(wù)最終也交給了RecyclerView.ItemAnimatorRecyclerView.ItemAnimator可由用戶自定義實(shí)現(xiàn)。

這里有必要說明一下,一些刪除或者新增操作,通過使用適配器中通知刪除或者新增的方法,最終還是會通知界面進(jìn)行重繪。

到這兒,我們可以總結(jié)一下,onLayout過程中,RecyclerView將子視圖布局的任務(wù)交給了LayoutMananger,同樣的,子視圖動畫也不是RecyclerView自身完成的,動畫任務(wù)被交給了RecyclerView.ItemAnimator,這也就解決了我們一開始提出的兩個問題:

  1. 布局多樣性的原因
  2. 布局動畫多樣性的原因

至于LayoutManagerRecyclerView.ItemAnimator更深層次的探討,我將會在后面的博客中進(jìn)行。

1.5 onDraw

RecylcerView中的onDraw方法比較簡單,僅僅繪制了ItemDecoration,同樣需要用戶自定義實(shí)現(xiàn):

public void onDraw(Canvas c) {
    super.onDraw(c);
    int count = this.mItemDecorations.size();
    for (int i = 0; i < count; ++i) {
        ((RecyclerView.ItemDecoration)this.mItemDecorations.get(i)).onDraw(c, this, this.mState);
    }
}

而子View的繪制其實(shí)在ViewGroup#dispatchDraw實(shí)現(xiàn)的,這里不再繼續(xù)討論了。

如果你沒看懂,沒關(guān)系,RecyclerView在三大工程流程中大概做了如下的事:

View的三大流程

2. View管理-Recycler

在上文中,我們簡要了解RecyclerView繪制的三大流程以及LayoutManagerItemAnimator承擔(dān)的任務(wù)。顯然,我們忽略了適配器Adapter和緩存管理Recycler,下面我們就重點(diǎn)談?wù)勥@兩位。

上文中,我們了解到在RecyclerView#dispatchLayoutStep2方法中,給子View定位的任務(wù)交給了LayoutManager

mLayout.onLayoutChildren(mRecycler, mState);

簡要的介紹一下LayoutManger#onLayoutChildren的工作內(nèi)容:

  1. 如果當(dāng)前RecyclerView中還存在子View,移除所有的子View,將移除的ViewHolder添加進(jìn)Recycler。
  2. 一次通過Recycler獲取一個子View。
  3. 重復(fù)進(jìn)行2,直到獲取的子View填充完RecyclerView即可。

雖然上面的內(nèi)容很簡單,但是LayoutManager的實(shí)際工作內(nèi)容要復(fù)雜的多,那么 Recycler工作機(jī)制是怎樣的呢?我們來一探究竟。

2.1 Recycler重要組成

先看組成部分:

緩存級別 參與對象 作用
一級緩存 mAttachedScrap、mChangedScrap mChangedScrap僅參與預(yù)布局,mAttachedScrap存放還會被復(fù)用的ViewHolder
二級緩存 mCachedViews 最多存放2個緩存ViewHolder
三級緩存 mViewCacheExtension 需開發(fā)者自定義實(shí)現(xiàn)
四級緩存 mRecyclerPool 可以理解RecyclerPool(int,ArrayList<ViewHolder>)SparseArray,鍵是viewType,每個viewType最多可以存放5個ViewHolder
2.2 獲取ViewHolder

入口是Recycler#getViewForPosition,有一個位置的參數(shù):

public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}

// 看函數(shù)名稱就知道,它是嘗試獲取ViewHolder
View getViewForPosition(int position, Boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

通過名字就可以猜到函數(shù)的意思了,ViewHolder中的itemView就是我們要獲取的子視圖,ViewHolder是如何獲取的呢?

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                Boolean dryRun, long deadlineNs) {
    //...
    ViewHolder holder = null;
    // 第一步 從 mChangedScrap 中獲取
    // PreLayout從名字可以看出,它不是真實(shí)的布局,不過我不是特別清楚
    // 預(yù)布局的意義。
    // 除此之外,它其實(shí)沒有意義的,沒有參與實(shí)際布局的緩存過程中。
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    // 第二步 從 mAttachedScrap或者mCachedViews 中獲取
    // 如果RecyclerView之前就有ViewHolder,并且這些ViewHolder之后還要
    // 繼續(xù)展現(xiàn),在Layout過程中,它會將這些ViewHolder先取出來存放進(jìn)mAttachedScrap,
    // 填充的時候再從mAttachedScrap取出
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        // ...
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 第三步 Find from scrap/cache via stable ids, if exists
        if (mAdapter.hasStableIds()) {
            // StableId可以被當(dāng)做ViewHolder的唯一標(biāo)識
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                                                    type, dryRun);
            //...
        }
        // 第四步 mViewCacheExtension需要用戶自定義實(shí)現(xiàn)并設(shè)置
        if (holder == null && mViewCacheExtension != null) {
            // We are NOT sending the offsetPosition because LayoutManager does not
            // know it.
            final View view = mViewCacheExtension
                                                    .getViewForPositionAndType(this, position, type);
            //...
        }
        if (holder == null) {
            // 第五步 從RecycledViewPool中獲取
            // 通過RecycledViewPool獲取
            // 每種ViewType的ViewHolder最多可以存放五個
            holder = getRecycledViewPool().getRecycledView(type);
            //...
        }
        if (holder == null) {
            // 第六步 緩存中都沒有就重新創(chuàng)建
            // 如果緩存中都沒有,就需要重新創(chuàng)建
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            // ...
        }
    }
    Boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // ...
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        // ...
        // 沒有綁定就重新綁定
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    // ...
    return holder;
}

從注釋中我們可以看到,前三步ViewHolder的獲取是利用的Recycler的一級緩存和二級緩存,第四步通過mViewCacheExtension獲取,第五步通過RecyuclerPool的方式獲取,如果連緩存池中都沒有,那么Recycler只好調(diào)用Adapter#createViewHolder重新創(chuàng)建,這個名稱是我們的老朋友了,而且還是在Adapter中,我們簡單了解一下Adapter#createViewHolder

public final VH createViewHolder(ViewGroup parent, int viewType) {
    // ...
    final VH holder = onCreateViewHolder(parent, viewType);
    holder.mItemViewType = viewType;
    // ...
    return holder;
}

public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);

真正創(chuàng)建ViewHolder的是Adapter#onCreateViewHolder方法,這也是我們繼承適配器Adapter必須要實(shí)現(xiàn)的抽象方法,通常,我們在繼承Adapter不會只創(chuàng)建ViewHolder,還會做子View和數(shù)據(jù)的綁定,在返回視圖之前,視圖的綁定肯定是完成了的,我們看看視圖綁定發(fā)生在哪里?

我們再返回上一個方法Recycler#tryGetViewHolderForPositionByDeadline中,可以看到在倒數(shù)第四行,在執(zhí)行Recycler#tryBindViewHolderByDeadline方法:

private Boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition,
                int position, long deadlineNs) {
    // ...
    // 最關(guān)鍵的方法就是調(diào)用了Adapter#bindViewHolder方法
    mAdapter.bindViewHolder(holder, offsetPosition);
    // ...
}

public void onBindViewHolder(VH holder, int position, List<Object> payloads) {
    onBindViewHolder(holder, position);
}

public abstract void onBindViewHolder(VH holder, int position);

成功見到我們必須實(shí)現(xiàn)的Adapter#onBindViewHolder方法,這些完成以后,子View就會被交給LayoutManager管理了。

2.2 回收ViewHolder

ViewHolder回收的場景有很多種,比如說滑動、數(shù)據(jù)刪除等等。我們在這里以滑動作為回收的場景,并且只分析手指觸摸時的滑動,滑動的入口在RecyclerView#onTouchEvent

public Boolean onTouchEvent(MotionEvent e) {
    // ...
    switch (action) {
        // ...
        case MotionEvent.ACTION_MOVE: {
            // ...
            if (mScrollState == SCROLL_STATE_DRAGGING) {
                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];
                // 當(dāng)前滑動狀態(tài)設(shè)置為SCROLL_STATE_DRAGGING 需要滑動距離大于閾值
                if (scrollByInternal(
                                            canScrollHorizontally ? dx : 0,
                                            canScrollVertically ? dy : 0,
                                            vtev)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                // ...
            }
        }
        break;
        // ...
    }
    // ...
    return true;
}

代碼簡化以后,我們僅需要關(guān)注RecyclerView#scrollByInternal

Boolean scrollByInternal(int x, int y, MotionEvent ev) {
    // ...
    if (mAdapter != null) {
        // ...
        // 無論是橫向或者縱向都交給了LayoutManager處理
        if (x != 0) {
            consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
            unconsumedX = x - consumedX;
        }
        if (y != 0) {
            consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
            unconsumedY = y - consumedY;
        }
        // ...
    }
    // ...
    return consumedX != 0 || consumedY != 0;
}

最后還是交給了LayoutManager處理,除去函數(shù)嵌套之后,最后又回到了LayoutManager的視圖填充的過程,在2.2章節(jié)中,我們僅僅討論了該過程中視圖的獲取,其實(shí),該過程中,還會涉及到視圖的回收,LayoutManager在回收的過程中,大概做了如下的事情:

  1. 找出需要回收的視圖。
  2. 通知父布局也就是RecyclerView移除子視圖。
  3. 通知Recycler進(jìn)行回收管理。

我們著重探究Recycler進(jìn)行回收管理,回收的入口是Recycler#recycleView

public void recycleView(View view) {
    // ...
    ViewHolder holder = getChildViewHolderint(view);
    // ...
    recycleViewHolderInternal(holder);
}

void recycleViewHolderInternal(ViewHolder holder) {
    // 一系列檢查
    // ...
    Boolean cached = false;
    Boolean recycled = false;
    // ...
    if (forceRecycle || holder.isRecyclable()) {
        if (mViewCacheMax > 0
                                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                                | ViewHolder.FLAG_REMOVED
                                | ViewHolder.FLAG_UPDATE
                                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
            // mViewCacheMax 默認(rèn)最大值為2
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                // 緩存數(shù)量大于2的時候?qū)⒆钕冗M(jìn)來的ViewHolder移除
                recycleCachedViewAt(0);
                cachedViewSize--;
            }
            // ...
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } else {
        // ...
    }
    // ViewInfoStore 中移除
    mViewInfoStore.removeViewHolder(holder);
}

從上述的Recycler#recycleViewHolderInternal方法可以看出,ViewHolder會被優(yōu)先加入mCachedViews,當(dāng)mCachedViews數(shù)量大于2的時候,會調(diào)用Recycler#recycleCachedViewAt方法:

void recycleCachedViewAt(int cachedViewIndex) {
    // ...
    ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
    // 添加進(jìn)緩存池RecyclerPool
    addViewHolderToRecycledViewPool(viewHolder, true);
    // 從mCachedViews中移除
    mCachedViews.remove(cachedViewIndex);
}

因?yàn)?code>cachedViewIndex是2,所以mCachedViewsViewHolder數(shù)量為2的時候,會先添加到mCachedViews,然后從mCachedViews中移除先進(jìn)來的ViewHolder添加進(jìn)緩存池。

我在這里選取了一些常用的場景,整合出如下圖片:


常見使用Recycler緩存場景

需要指明的是:

  1. mChangedScrap實(shí)際并未參加真實(shí)的緩存過程,它的添加和移除ViewHolder都出現(xiàn)在RecyclerView#dispatchLayoutStep1方法中的PreLayout(預(yù)布局)過程中。
  2. 對于RecyclerView中已經(jīng)顯示并將繼續(xù)展示的ViewHolder,重繪過程中,會將ViewHolder以及其中的子ViewRecyclerView移出,添加進(jìn)mAttachedScrap中,并在后續(xù)的填充子View過程中,從mAttachedScrap取出。
  3. mCachedViews最多只能緩存兩個ViewHolder,如果大于最大緩存數(shù)量,會將先進(jìn)來的ViewHolder取出加入RecycledViewPool
  4. RecycledViewPool針對每種viewTypeViewHolder提供最大最大數(shù)量為5的緩存。

有了Recycler以后:

android手機(jī)界面首頁.jpg

灰色的是小T同學(xué)的手機(jī)屏幕,查看聊天記錄的時候,RecyclerView不會每次都創(chuàng)建新的ViewHolder,也不會一次性將所有的ViewHolder都建好,減少了內(nèi)存和時間的損耗,所以,小T同學(xué)就可以流暢的查看和女友的上千條聊天記錄了~

三、淺談設(shè)計(jì)模式

閱讀源碼的過程中,發(fā)現(xiàn)RecyclerView運(yùn)用了很多設(shè)計(jì)模式。

Adapter類這個名字,就可以看出它使用了適配器模式,因?yàn)樯婕暗綄?shù)據(jù)集轉(zhuǎn)變成RecyclerView需要的子視圖。除了適配器模式之外,Adapter中還使用觀察者模式,這一點(diǎn)可以從RecyclerView#setAdapter方法中可以看出,設(shè)置適配器的時候,會對舊的Adapter取消注冊監(jiān)聽器,接著對新的Adapter注冊監(jiān)聽器,等到數(shù)據(jù)發(fā)生變化的時候,通知給觀察者,觀察者就可以在RecyclerView內(nèi)愉快地刪除或者新增子視圖了。

接著,看LayoutManager這個類,RecyclerView將給View布局這個任務(wù)交給了抽象類LayoutManager,根據(jù)不同需求,比如線性布局可以用LinearLayoutManager實(shí)現(xiàn),網(wǎng)格布局可以用GridLayoutManager。應(yīng)對同一個布局問題,RecyclerView使用了策略模式,給出了不同的解決方案,ItemAnimator也是如此。

如果感興趣的話,同學(xué)們可以查看對應(yīng)的源碼。

四、總結(jié)

本文中,除了對Recycler進(jìn)行深層次研究外,其他則點(diǎn)到為止,大致得到如下結(jié)論:

總結(jié)

后續(xù)博客中,我將和大家一起學(xué)習(xí)RecyclerView中的其他部分。這大概是我寫的最難受的博客之一了,一是RecyclerView的源碼很長,看著有點(diǎn)累;二是源碼分析的博客確實(shí)不知道怎么寫,還在持續(xù)探索中。本人水平有限,難免有誤,歡迎指出喲~

如果你對本系列文章感興趣

第一篇:《抽絲剝繭RecyclerView - 化整為零》

參考文章

《RecyclerView 源碼解析》
《RecyclerView緩存原理,有圖有真相》
《RecyclerView緩存機(jī)制(咋復(fù)用?)》
《RecyclerView動畫源碼淺析》
《Understanding RecyclerView Components. Part -2》

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

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

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