Android RecycleView原始碼分析

weixin_33936401發表於2017-08-25

之前寫過一篇關於《RecycleView 實現複雜首頁佈局》的文章,最近因為專案需要,重新閱讀RecycleView原始碼收穫頗多,今天給大家詳解一下。

基本使用

1 接入方式

compile 'com.android.support:recyclerview-v7:24.0.0'

2 建立物件

RecyclerView recyclerview = (RecyclerView)findViewById(R.id.recyclerview);

3 設定顯示規則

recyclerview.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
RecyclerView 將所有的顯示規則交給一個叫 LayoutManager 的類去完成了。
LayoutManager 是一個抽象類,系統已經為我們提供了三個預設的實現類,分別是 LinearLayoutManager、 GridLayoutManager 、 StaggeredGridLayoutManager,從名字我們就能看出來了,分別是,線性顯示、網格顯示、瀑布流顯示。當然你也可以通過繼承這些類來擴充套件實現自己的 LayougManager。

4 設定介面卡

recyclerview.setAdapter(adapter);
介面卡,同 ListView 一樣,用來設定每個item顯示內容的。 
通常,我們寫 ListView 介面卡,都是首先繼承 BaseAdapter,實現四個抽象方法,建立一個靜態ViewHolder , getView() 方法中判斷 convertView 是否為空,建立還是獲取 viewholder 物件。
而 RecyclerView 也是類似的步驟,首先繼承RecyclerView.Adapter<VH>類,實現三個抽象方法,建立一個靜態的 ViewHolder。不過 RecyclerView 的 ViewHolder 建立稍微有些限制,類名就是上面繼承的時候泛型中宣告的類名(好像反了,應該是上面泛型中的類名應該是這個holder的類名);並且 ViewHolder 必須繼承自RecyclerView.ViewHolder類。

繪製流程

眾所周知,android控制元件的繪製可以分為3個步驟:measure、layout、draw。RecyclerView的繪製自然也經這3個步驟。但是,RecyclerView將它的measure與layout過程委託給了RecyclerView.LayoutManager來處理,並且,它對子控制元件的measure及layout過程是逐個處理的,也就是說,執行完成一個子控制元件的measure及layout過程再去執行下一個。下面看下這段程式碼:

  • 繪製方法onMeasure
protected void onMeasure(int widthSpec, int heightSpec) {
    ...
    if (mLayout.mAutoMeasure) {
        final int widthMode = MeasureSpec.getMode(widthSpec);
        final int heightMode = MeasureSpec.getMode(heightSpec);
        final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                && heightMode == MeasureSpec.EXACTLY;
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
        if (skipMeasure || mAdapter == null) {
            return;
        }
        ...
        dispatchLayoutStep2();

        mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
        ...
    } else {
        ...
    }
}

這是RecyclerView的測量方法,再看下dispatchLayoutStep2()方法:

private void dispatchLayoutStep2() {
    ...
    mLayout.onLayoutChildren(mRecycler, mState);
    ...
}

上面的mLayout就是一個RecyclerView.LayoutManager例項。通過以上程式碼(和方法名稱),不難推斷出,RecyclerView的measure及layout過程委託給了RecyclerView.LayoutManager。接著看onLayoutChildren方法,在相容包中提供了3個RecyclerView.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.
    ...
    mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
    // calculate anchor position and coordinate
    updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
    ...
    if (mAnchorInfo.mLayoutFromEnd) {
        ...
    } else {
        // fill towards end
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtra = extraForEnd;
        fill(recycler, mLayoutState, state, false);
        endOffset = mLayoutState.mOffset;
        final int lastElement = mLayoutState.mCurrentPosition;
        if (mLayoutState.mAvailable > 0) {
            extraForStart += mLayoutState.mAvailable;
        }
        // fill towards start
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtra = extraForStart;
        mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
        fill(recycler, mLayoutState, state, false);
        startOffset = mLayoutState.mOffset;
        ...
    }
    ...
}

原始碼中的註釋部分我並沒有略去,它已經解釋了此處的邏輯了。這裡我以垂直佈局來說明,mAnchorInfo為佈局錨點資訊,包含了子控制元件在Y軸上起始繪製偏移量(coordinate),ItemView在Adapter中的索引位置(position)和佈局方向(mLayoutFromEnd)——這裡是指start、end方向。這部分程式碼的功能就是:確定佈局錨點,以此為起點向開始和結束方向填充ItemView,如圖所示:

3278692-70fb1c66e0f3a491.png
image.png

在上一段程式碼中,fill()方法的作用就是填充ItemView,而圖(3)說明了,在上段程式碼中fill()方法呼叫2次的原因。雖然圖(3)是更為普遍的情況,而且在實現填充ItemView演算法時,也是按圖(3)所示來實現的,但是mAnchorInfo在賦值過程(updateAnchorInfoForLayout)中,只會出現圖(1)、圖(2)所示情況。現在來看下fill()方法:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    ...
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    LayoutChunkResult layoutChunkResult = new LayoutChunkResult();
    while (...&&layoutState.hasMore(state)) {
        ...
        layoutChunk(recycler, state, layoutState, layoutChunkResult);

        ...
        if (...) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            remainingSpace -= layoutChunkResult.mConsumed;
        }
        if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
            layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }
    }
    ...
}

下面是layoutChunk()方法:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    View view = layoutState.next(recycler);
    ...
    if (layoutState.mScrapList == null) {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            addView(view);
        } else {
            addView(view, 0);
        }
    }
    ...
    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.
    layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
            right - params.rightMargin, bottom - params.bottomMargin);
    ...
}

這裡的addView()方法,其實就是ViewGroup的addView()方法;measureChildWithMargins()方法看名字就知道是用於測量子控制元件大小的,這裡我先跳過這個方法的解釋,放在後面來做,目前就簡單地理解為測量子控制元件大小就好了。下面是layoutDecoreated()方法:

public void layoutDecorated(...) {
        ...
        child.layout(...);
}

總結上面程式碼,在RecyclerView的measure及layout階段,填充ItemView的演算法為:向父容器增加子控制元件,測量子控制元件大小,佈局子控制元件,佈局錨點向當前佈局方向平移子控制元件大小,重複上訴步驟至RecyclerView可繪製空間消耗完畢或子控制元件已全部填充。
  這樣所有的子控制元件的measure及layout過程就完成了。回到RecyclerView的onMeasure方法,執行mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec)這行程式碼的作用就是根據子控制元件的大小,設定RecyclerView的大小。至此,RecyclerView的measure和layout實際上已經完成了。
  但是,你有可能已經發現上面過程中的問題了:如何確定RecyclerView的可繪製空間?不過,如果你熟悉android控制元件的繪製機制的話,這就不是問題。其實,這裡的可繪製空間,可以簡單地理解為父容器的大小;更準確的描述是,父容器對RecyclerView的佈局大小的要求,可以通過MeasureSpec.getSize()方法獲得——這裡不包括滑動情況,滑動情況會在後文描述。需要特別說明的是在23.2.0版本之前,RecyclerView是不支援WRAP_CONTENT的。

  • 佈局onLayout()
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    ...
    dispatchLayout();
    ...
}

這是dispatchLayout()方法:

void dispatchLayout() {
    ...
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        ...
        dispatchLayoutStep2();
    }
    dispatchLayoutStep3();
    ...
}

可以看出,這裡也會執行子控制元件的measure及layout過程。結合onMeasure方法對skipMeasure的判斷可以看出,如果要支援WRAP_CONTENT,那麼子控制元件的measure及layout就會提前在RecyclerView的測量方法中執行完成,也就是說,先確定了子控制元件的大小及位置後,再由此設定RecyclerView的大小;如果是其它情況(測量模式為EXACTLY),子控制元件的measure及layout過程就會延遲至RecyclerView的layout過程(RecyclerView.onLayout())中執行。再看onMeasure方法中的mLayout.mAutoMeasure,它表示,RecyclerView的measure及layout過程是否要委託給RecyclerView.LayoutManager,在相容包中提供的3種RecyclerView.LayoutManager的這個屬性預設都是為true的。好了,以上就是RecyclerView的measure及layout過程,下面來看下它的draw過程。
  RecyclerView的draw過程可以分為2部分來看:RecyclerView負責繪製所有decoration;ItemView的繪製由ViewGroup處理,這裡的繪製是android常規繪製邏輯,本文就不再闡述了。

  • 繪製 draw()和onDraw()
@Override
public void draw(Canvas c) {
    super.draw(c);

    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDrawOver(c, this, mState);
    }
    ...
}

@Override
public void onDraw(Canvas c) {
    super.onDraw(c);

    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDraw(c, this, mState);
    }
}

可以看出對於decoration的繪製程式碼上十分簡單。但是這裡,我必須要抱怨一下RecyclerView.ItemDecoration的設計,它實在是太過於靈活了,雖然理論上我們可以使用它在RecyclerView內的任何地方繪製你想要的任何東西——到這一步,RecyclerView的大小位置已經確定的哦。但是過於靈活,太難使用,以至往往使我們無從下手。
好了,題外話就不多說了,來看看decoration的繪製吧。還記得上面提到過的measureChildWithMargins()方法嗎?先來看看它:

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 = ...
        final int heightSpec = ...
        if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
            child.measure(widthSpec, heightSpec);
        }
    }

這裡是getItemDecorInsetsForChild()方法:

Rect getItemDecorInsetsForChild(View child) {
    ...
    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);
        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;
}

方法getItemOffsets()就是我們在實現一個RecyclerView.ItemDecoration時可以重寫的方法,通過mTempRect的大小,可以為每個ItemView設定位置偏移量,這個偏移量最終會參與計算ItemView的大小,也就是說ItemView的大小是包含這個位置偏移量的。我們在重寫getItemOffsets()時,可以指定任意數值的偏移量:

3278692-b9494bd2623d643d.png
image.png

4個方向的位置偏移量對應mTempRect的4個屬性(left,top,right,bottom),我以top offset的值在垂直線性佈局中的應用來舉例說明下。如果top offset等於0,那麼ItemView之間就沒有空隙;如果top offset大於0,那麼ItemView之前就會有一個間隙;如果top offset小於0,那麼ItemView之間就會有重疊的區域。
  當然,我們在實現RecyclerView.ItemDecoration時,並不一定要重寫getItemOffsets(),同樣的對於RecyclerView.ItemDecoration.onDraw()或RecyclerView.ItemDecoration.onDrawOver()方法也不是一定要重寫,而且,這個繪製方法和我們所設定的位置偏移量沒有任何聯絡。下面我來實現一個RecyclerView.ItemDecoration來加深下這裡的理解:我將在垂直線性佈局下,在ItemView間繪製一條5個畫素寬、只有ItemView一半長、與ItemView居中對齊的紅色分割線,這條分割線在ItemView內部top位置。

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
  Paint paint = new Paint();
  paint.setColor(Color.RED);

  for (int i = 0; i < parent.getLayoutManager().getChildCount(); i++) {
    final View child = parent.getChildAt(i);

    float left = child.getLeft() + (child.getRight() - child.getLeft()) / 4;
    float top = child.getTop();
    float right = left + (child.getRight() - child.getLeft()) / 2;
    float bottom = top + 5;

    c.drawRect(left,top,right,bottom,paint);
  }
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
  outRect.set(0, 0, 0, 0);
}

以上就是RecyclerView的整個繪製流程了

滑動流程

RecyclerView的滑動過程可以分為2個階段:手指在螢幕上移動,使RecyclerView滑動的過程,可以稱為scroll;手指離開螢幕,RecyclerView繼續滑動一段距離的過程,可以稱為fling。現在先看看RecyclerView的觸屏事件處理onTouchEvent()方法:

public boolean onTouchEvent(MotionEvent e) {
    ...
    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    ...
    switch (action) {
        ...
        case MotionEvent.ACTION_MOVE: {
            ...
            final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
            final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
            int dx = mLastTouchX - x;
            int dy = mLastTouchY - y;
            ...
            if (mScrollState != SCROLL_STATE_DRAGGING) {
                ...
                if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                    if (dy > 0) {
                        dy -= mTouchSlop;
                    } else {
                        dy += mTouchSlop;
                    }
                    startScroll = true;
                }
                if (startScroll) {
                    setScrollState(SCROLL_STATE_DRAGGING);
                }
            }

            if (mScrollState == SCROLL_STATE_DRAGGING) {
                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];

                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        vtev)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
            }
        } break;
        ...
        case MotionEvent.ACTION_UP: {
            ...
            final float yvel = canScrollVertically ?
                    -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
            if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                setScrollState(SCROLL_STATE_IDLE);
            }
            resetTouch();
        } break;
        ...
    }
    ...
}

這裡我以垂直方向的滑動來說明。當RecyclerView接收到ACTION_MOVE事件後,會先計算出手指移動距離(dy),並與滑動閥值(mTouchSlop)比較,當大於此閥值時將滑動狀態設定為SCROLL_STATE_DRAGGING,而後呼叫scrollByInternal()方法,使RecyclerView滑動,這樣RecyclerView的滑動的第一階段scroll就完成了;當接收到ACTION_UP事件時,會根據之前的滑動距離與時間計算出一個初速度yvel,這步計算是由VelocityTracker實現的,然後再以此初速度,呼叫方法fling(),完成RecyclerView滑動的第二階段fling。顯然滑動過程中關鍵的方法就2個:scrollByInternal()與fling()。接下來同樣以垂直線性佈局來說明。先來說明scrollByInternal(),跟蹤進入後,會發現它最終會呼叫到LinearLayoutManager.scrollBy()方法,這個過程很簡單,我就不列出原始碼了,但是分析到這裡先暫停下,去看看fling()方法:

public boolean fling(int velocityX, int velocityY) {
    ...
    mViewFlinger.fling(velocityX, velocityY);
    ...
}

有用的就這一行,其它亂七八糟的不看也罷。mViewFlinger是一個Runnable的實現ViewFlinger的物件,就是它來控制元件著ReyclerView的fling過程的演算法的。下面來看下類ViewFlinger的一段程式碼:

void postOnAnimation() {
    if (mEatRunOnAnimationRequest) {
        mReSchedulePostAnimationCallback = true;
    } else {
        removeCallbacks(this);
        ViewCompat.postOnAnimation(RecyclerView.this, this);
    }
}

public void fling(int velocityX, int velocityY) {
    setScrollState(SCROLL_STATE_SETTLING);
    mLastFlingX = mLastFlingY = 0;
    mScroller.fling(0, 0, velocityX, velocityY,
            Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
    postOnAnimation();
}

可以看到,其實RecyclerView的fling是藉助Scroller實現的;然後postOnAnimation()方法的作用就是在將來的某個時刻會執行我們給定的一個Runnable物件,在這裡就是這個mViewFlinger物件,這部分原理我就不再深入分析了,它已經不屬於本文的範圍了。並且,關於Scroller的作用及原理,本文也不會作過多解釋。對於這兩點各位可以自行查閱,有很多文章對於作過詳細闡述的。接下來看看ViewFlinger.run()方法:

public void run() {
    ...
    if (scroller.computeScrollOffset()) {
        final int x = scroller.getCurrX();
        final int y = scroller.getCurrY();
        final int dx = x - mLastFlingX;
        final int dy = y - mLastFlingY;
        ...
        if (mAdapter != null) {
            ...
            if (dy != 0) {
                vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
                overscrollY = dy - vresult;
            }
            ...
        }
        ...
        if (!awakenScrollBars()) {
            invalidate();//重新整理介面
        }
        ...
        if (scroller.isFinished() || !fullyConsumedAny) {
            setScrollState(SCROLL_STATE_IDLE);
        } else {
            postOnAnimation();
        }
    }
    ...
}

本段程式碼中有個方法mLayout.scrollVerticallyBy(),跟蹤進入你會發現它最終也會走到LinearLayoutManager.scrollBy(),這樣雖說RecyclerView的滑動可以分為兩階段,但是它們的實現最終其實是一樣的。這裡我先解釋下上段程式碼。第一,dy表示滑動偏移量,它是由Scroller根據時間偏移量(Scroller.fling()開始時間到當前時刻)計算出的,當然如果是RecyclerView的scroll階段,這個偏移量也就是手指滑動距離。第二,上段程式碼會多次執行,至到Scroller判斷滑動結束或已經滑動到邊界。再多說一下,postOnAnimation()保證了RecyclerView的滑動是流暢,這裡涉及到著名的“android 16ms”機制,簡單來說理想狀態下,上段程式碼會以16毫秒一次的速度執行,這樣其實,Scroller每次計算的滑動偏移量是很小的一部分,而RecyclerView就會根據這個偏移量,確定是平移ItemView,還是除了平移還需要再建立新ItemView。

3278692-ed25a05bbf35cbfa.png
image.png

現在就來看看LinearLayoutManager.scrollBy()方法:

int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    final int absDy = Math.abs(dy);
    updateLayoutState(layoutDirection, absDy, true, state);
    final int consumed = mLayoutState.mScrollingOffset
            + fill(recycler, mLayoutState, state, false);
    ...
    final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
    mOrientationHelper.offsetChildren(-scrolled);
    ...
}

如上文所講到的fill()方法,作用就是向可繪製區間填充ItemView,那麼在這裡,可繪製區間就是滑動偏移量!再看方法mOrientationHelper.offsetChildren()作用就是平移ItemView。

回收與複用

Recycler的作用就是重用ItemView。在填充ItemView的時候,ItemView是從它獲取的;滑出螢幕的ItemView是由它回收的。對於不同狀態的ItemView儲存在了不同的集合中,比如有scrapped、cached、exCached、recycled,當然這些集合並不是都定義在同一個類裡。
  回到之前的layoutChunk方法中,有行程式碼layoutState.next(recycler),它的作用自然就是獲取ItemView,我們進入這個方法檢視,最終它會呼叫到RecyclerView.Recycler.getViewForPosition()方法:

View getViewForPosition(int position, boolean dryRun) {
    ...
    // 0) If there is a changed scrap, try to find from there
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrap = holder != null;
    }
    // 1) Find from scrap by position
    if (holder == null) {
        holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
        ...
    }
    if (holder == null) {
        ...
        // 2) Find from scrap via stable ids, if exists
        if (mAdapter.hasStableIds()) {
            holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
            ...
        }
        if (holder == null && mViewCacheExtension != null) {
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
                ...
            }
        }
        if (holder == null) {
            ...
            holder = getRecycledViewPool().getRecycledView(type);
            ...
        }
        if (holder == null) {
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            ...
        }
    }
    ...
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // do not update unless we absolutely have to.
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        ...
        mAdapter.bindViewHolder(holder, offsetPosition);
        ...
    }
    ...
}

這個方法比較長,我先解釋下它的邏輯吧。根據列表位置獲取ItemView,先後從scrapped、cached、exCached、recycled集合中查詢相應的ItemView,如果沒有找到,就建立(Adapter.createViewHolder()),最後與資料集繫結。其中scrapped、cached和exCached集合定義在RecyclerView.Recycler中,分別表示將要在RecyclerView中刪除的ItemView、一級快取ItemView和二級快取ItemView,cached集合的大小預設為2,exCached是需要我們通過RecyclerView.ViewCacheExtension自己實現的,預設沒有;recycled集合其實是一個Map,定義在RecyclerView.RecycledViewPool中,將ItemView以ItemType分類儲存了下來,這裡算是RecyclerView設計上的亮點,通過RecyclerView.RecycledViewPool可以實現在不同的RecyclerView之間共享ItemView,只要為這些不同RecyclerView設定同一個RecyclerView.RecycledViewPool就可以了。
上面解釋了ItemView從不同集合中獲取的方式,那麼RecyclerView又是在什麼時候向這些集合中新增ItemView的呢?下面我逐個介紹下。
scrapped集合中儲存的其實是正在執行REMOVE操作的ItemView,這部分會在後文進一步描述。
在fill()方法的迴圈體中有行程式碼recycleByLayoutState(recycler, layoutState);,最終這個方法會執行到RecyclerView.Recycler.recycleViewHolderInternal()方法:

void recycleViewHolderInternal(ViewHolder holder) {
        ...
        if (forceRecycle || holder.isRecyclable()) {
            if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED
                    | ViewHolder.FLAG_UPDATE)) {
                // Retire oldest cached view
                final int cachedViewSize = mCachedViews.size();
                if (cachedViewSize == mViewCacheMax && cachedViewSize > 0) {
                    recycleCachedViewAt(0);
                }
                if (cachedViewSize < mViewCacheMax) {
                    mCachedViews.add(holder);
                    cached = true;
                }
            }
            if (!cached) {
                addViewHolderToRecycledViewPool(holder);
                recycled = true;
            }
        }
        ...
    }

這個方法的邏輯是這樣的:首先判斷集合cached是否満了,如果已満就從cached集合中移出一個到recycled集合中去,再把新的ItemView新增到cached集合;如果不満就將ItemView直接新增到cached集合。
最後exCached集合是我們自己建立的,所以新增刪除元素也要我們自己實現。

資料集、動畫

RecyclerView定義了4種針對資料集的操作,分別是ADD、REMOVE、UPDATE、MOVE,封裝在了AdapterHelper.UpdateOp類中,並且所有操作由一個大小為30的物件池管理著。當我們要對資料集作任何操作時,都會從這個物件池中取出一個UpdateOp物件,放入一個等待佇列中,最後呼叫RecyclerView.RecyclerViewDataObserver.triggerUpdateProcessor()方法,根據這個等待佇列中的資訊,對所有子控制元件重新測量、佈局並繪製且執行動畫。以上就是我們呼叫Adapter.notifyItemXXX()系列方法後發生的事。
顯然當我們對某個ItemView做操作時,它很有可以會影響到其它ItemView。下面我以REMOVE為例來梳理下這個流程。

3278692-5e1e610f42b6e4ef.png
image.png

首先呼叫Adapter.notifyItemRemove(),追溯到方法RecyclerView.RecyclerViewDataObserver.onItemRangeRemoved():

public void onItemRangeRemoved(int positionStart, int itemCount) {
    assertNotInLayoutOrScroll(null);
    if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
        triggerUpdateProcessor();
    }
}

這裡的mAdapterHelper.onItemRangeRemoved()就是向之前提及的等待佇列新增一個型別為REMOVE的UpdateOp物件, triggerUpdateProcessor()方法就是呼叫View.requestLayout()方法,這會導致介面重新佈局,也就是說方法RecyclerView.onLayout()會隨後呼叫,這之後的流程就和在繪製流程一節中所描述的一致了。但是動畫在哪是執行的呢?檢視之前所列出的onLayout()方法發現dispatchLayoutStepX方法共有3個,前文只解釋了dispatchLayoutStep2()的作用,這裡就其它2個方法作進一步說明。不過dispatchLayoutStep1()沒有過多要說明的東西,它的作用只是初始化資料,需要詳細說明的是dispatchLayoutStep3()方法:

private void dispatchLayoutStep3() {
    ...
    if (mState.mRunSimpleAnimations) {
        // Step 3: Find out where things are now, and process change animations.
        ...
        // Step 4: Process view info lists and trigger animations
        mViewInfoStore.process(mViewInfoProcessCallback);
    }
    ...
}

程式碼註釋已經說明得很清楚了,這裡我沒有列出step 3相關的程式碼是因為這部分只是初始化或賦值一些執行動畫需要的中間資料,process()方法最終會執行到RecyclerView.animateDisappearance()方法:

private void animateDisappearance(...) {
    addAnimatingView(holder);
    holder.setIsRecyclable(false);
    if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
        postAnimationRunner();
    }
}

這裡的animateDisappearance()會把一個動畫與ItemView繫結,並新增到待執行佇列中, postAnimationRunner()呼叫後就會執行這個佇列中的動畫,注意方法addAnimatingView():

private void addAnimatingView(ViewHolder viewHolder) {
    final View view = viewHolder.itemView;
    ...
    mChildHelper.addView(view, true);
    ...
}

這裡最終會向ChildHelper中的一個名為mHiddenViews的集合新增給定的ItemView,那麼這個mHiddenViews又是什麼東西?上節中的getViewForPosition()方法中有個getScrapViewForPosition(),作用是從scrapped集合中獲取ItemView:

ViewHolder getScrapViewForPosition(int position, int type, boolean dryRun) {
    ...
    View view = mChildHelper.findHiddenNonRemovedView(position, type);
    ...
}

接下來是findHiddenNonRemovedView()方法:

View findHiddenNonRemovedView(int position, int type) {
    final int count = mHiddenViews.size();
    for (int i = 0; i < count; i++) {
        final View view = mHiddenViews.get(i);
        RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
        if (holder.getLayoutPosition() == position && !holder.isInvalid() && !holder.isRemoved()
                && (type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) {
            return view;
        }
    }
    return null;
}

Recycler快取小結

public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
private ArrayList<ViewHolder> mChangedScrap = null;

final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

private final List<ViewHolder>
        mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

private RecycledViewPool mRecyclerPool;

private ViewCacheExtension mViewCacheExtension;

這麼多的集合,還有什麼Pool,ViewCache。看來他就是一個超大型的快取器了。
事實上他確實就是一個超大型的快取器,擁有三級快取(如果算上建立的那一次,應該是四級了),這麼大的快取系統,究竟是如何完成的?

  • 第一級快取:
    就是上面的一系列 mCachedViews。如果仍依賴於 RecyclerView (比如已經滑動出可視範圍,但還沒有被移除掉),但已經被標記移除的 ItemView 集合會被新增到 mAttachedScrap 中。然後如果 mAttachedScrap 中不再依賴時會被加入到 mCachedViews 中。 mChangedScrap 則是儲存 notifXXX 方法時需要改變的 ViewHolder 。

  • 第二級快取:
    ViewCacheExtension 是一個抽象靜態類,用於充當附加的快取池,當 RecyclerView 從第一級快取找不到需要的 View 時,將會從 ViewCacheExtension 中找。不過這個快取是由開發者維護的,如果沒有設定它,則不會啟用。通常我們也不會去設定他,系統已經預先提供了兩級快取了,除非有特殊需求,比如要在呼叫系統的快取池之前,返回一個特定的檢視,才會用到他。

  • 第三級快取:
    最強大的快取器。之前講了,與 ListView 直接快取 ItemView 不同,從上面程式碼裡我們也能看到,RecyclerView 快取的是 ViewHolder。而 ViewHolder 裡面包含了一個 View 這也就是為什麼在寫 Adapter 的時候 必須繼承一個固定的 ViewHolder 的原因。首先來看一下 RecycledViewPool:

public static class RecycledViewPool {
 
 // 根據 viewType 儲存的被廢棄的 ViewHolder 集合,以便下次使用
 private SparseArray<ArrayList<ViewHolder>> mScrap = new SparseArray<ArrayList<ViewHolder>>();
  /**
   * 從快取池移除並返回一個 ViewHolder
   */
  public ViewHolder getRecycledView(int viewType) {
    final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
    if (scrapHeap != null && !scrapHeap.isEmpty()) {
      final int index = scrapHeap.size() - 1;
      final ViewHolder scrap = scrapHeap.get(index);
      scrapHeap.remove(index);
      return scrap;
    }
      return null;
    }
 
  public void putRecycledView(ViewHolder scrap) {
    final int viewType = scrap.getItemViewType();
    final ArrayList scrapHeap = getScrapHeapForType(viewType);
    if (mMaxScrap.get(viewType) <= scrapHeap.size()) {
      return;
    }
    scrap.resetInternal();
    scrapHeap.add(scrap);
  }
 
  /**
   * 根據 viewType 獲取對應快取池
   */
  private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {
    ArrayList<ViewHolder> scrap = mScrap.get(viewType);
      if (scrap == null) {
        scrap = new ArrayList<>();
        mScrap.put(viewType, scrap);
          if (mMaxScrap.indexOfKey(viewType) < 0) {
            mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);
          }
      }
    return scrap;
  }
}

從名字來看,他是一個快取池,實現上,是通過一個預設為 5 大小的 ArrayList 實現的。這一點,同 ListView 的 RecyclerBin 這個類一樣。很奇怪為什麼不用 LinkedList 來做,按理說這種不需要索引讀取的快取池,用連結串列是最合適的。
然後每一個 ArrayList 又都是放在一個 Map 裡面的,SparseArray 這個類我們在講效能優化的時候已經多次提到了,就是兩個陣列,用來替代 Map 的。
把所有的 ArrayList 放在一個 Map 裡面,這也是 RecyclerView 最大的亮點,這樣根據 itemType 來取不同的快取 Holder,每一個 Holder 都有對應的快取,而只需要為這些不同 RecyclerView 設定同一個 Pool 就可以了。
這一點我們在 Pool 的 setter 方法上可以看到註釋:

/**
 * Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views.
 * This can be useful if you have multiple RecyclerViews with adapters that use the same
 * view types, for example if you have several data sets with the same kinds of item views
 * displayed by a {@link android.support.v4.view.ViewPager ViewPager}.
 *
 * @param pool Pool to set. If this parameter is null a new pool will be created and used.
 */
public void setRecycledViewPool(RecycledViewPool pool) {
    mRecycler.setRecycledViewPool(pool);
}

在類似 ViewPager 這種檢視中,所有 RecyclerView 的 holder 是共存於同一個 Pool 中的。

總結

以上是我閱讀原始碼總結的一些知識點,我從測量,繪製,佈局,快取四個方面做了比較詳細的說明。除這些之外,RecyclerView 還有其他好多知識點,例如:Item拖動。下節文章將實現《實現item的拖動刪除,資料儲存》等,當然這些資訊網上部落格很多,僅供參考。

相關文章