深入理解Android中的快取機制(二)RecyclerView跟ListView快取機制對比

wustor發表於2018-02-03

概述

前面介紹過了,記憶體快取主要是指在內部儲存器儲存資料,可能大家聽得比較多的就是LruCache演算法,裡面會涉及到記憶體快取,下面以就以Android中比較常見的兩個控制元件,ListView/GridView跟RecyclerView來分析一下他們是如何通過快取複用Item,來展示大量資料,由於ListView已經有很多人分析過,其中郭霖早期寫了一篇文章Android ListView工作原理完全解析,帶你從原始碼的角度徹底理解,已經分析的很到位了,所以就不再說ListView/GridView的繪製流程了,下面會重點分析一些ListView/GridView跟GridView的快取原理,由於他們都是繼承自AbsListView,所以快取邏輯很自然地是在AbsListView中實現的,下面就來對一下AbsListView的快取原理:

  • AbsListView的快取機制:如何複用Item
  • RecyclerView的快取機制:如何複用Item
  • 定向重新整理:RecyclerView如何定向重新整理Item
  • 區域性重新整理:RecyclerView如何實現區域性重新整理Item
  • DiffUtil:如何找出新舊集合的差異

AbsListView

註釋

AbsListView是一個抽象類,官方註釋是這麼寫的

Base class that can be used to implement virtualized lists of items. A list does not have a spatial definition here. For instance, subclases of this class can display the content of the list in a grid, in a carousel, as stack, etc.
複製程式碼

用於展示大量資料的item的基類。在這裡並沒有指定List的展現方式。舉例來說,這個類的子類可以在網格,列表中展示大量的資料

通過註釋可以很明顯的知道AbsListView作為GridView以及ListView的基類,沒有固定展示資料的形式,這個是交由他的子類來實現的,只是ListView是列表,GridView是網格,下面開始從原始碼的角度來分析一下AbsListView的快取機制。

繼承關係

AdapterView

GridView跟ListView並列,繼承自AbsListView,然而AbsListView又是跟AdapterViewAnimator,AbsSpinner是同類的,從這裡可以看出,快取的邏輯應該為GridView跟ListView共有,所以應該是在AbsListView中進行整合的,所以我們重點關注AbsListView這個類就可以了。AbsListView繼承自ViewGroup,那麼很自然地就會進行onMeasure,onLayout方法,下面就跟著這兩個方法來進行 分析。

onMeasure

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mSelector == null) {
        useDefaultSelector();
    }
   //計算padding
    final Rect listPadding = mListPadding;
    listPadding.left = mSelectionLeftPadding + mPaddingLeft;
    listPadding.top = mSelectionTopPadding + mPaddingTop;
    listPadding.right = mSelectionRightPadding + mPaddingRight;
    listPadding.bottom = mSelectionBottomPadding + mPaddingBottom;
    // 如果ranscriptMode是TRANSCRIPT_MODE_NORMAL,
    //當Adapter中的資料集改變之後,其子類會自動滾動到底部
    if (mTranscriptMode == TRANSCRIPT_MODE_NORMAL) {
        final int childCount = getChildCount();
        final int listBottom = getHeight() - getPaddingBottom();
        final View lastChild = getChildAt(childCount - 1);
        final int lastBottom = lastChild != null ? lastChild.getBottom() : listBottom;
        mForceTranscriptScroll = mFirstPosition + childCount >= mLastHandledItemCount &&
                lastBottom <= listBottom;
    }
}
複製程式碼

onLayout

子類不能覆蓋onLayout方法,需要重寫layoutChildren方法

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    mInLayout = true;
    final int childCount = getChildCount();
    if (changed) {
        for (int i = 0; i < childCount; i++) {
            getChildAt(i).forceLayout();
        }
      //重新測量Child,mRecycler其實就是RecycleBin,
      //這個是一個用於管理回收的View的回收類,一會兒單獨分析
        mRecycler.markChildrenDirty();
    }
   //給Child佈局,一會兒會單獨分析
    layoutChildren();
    mInLayout = false;
    mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
    // TODO: Move somewhere sane. This doesn't belong in onLayout().
    if (mFastScroll != null) {
        mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
    }
}
複製程式碼

RecycleBin

在看RecyclerBin原始碼之前,我們可以感性地分析一下,一般的快取分為初始化以及清空快取,實際上RecycleBin實際上也大體分這幾個步驟。

成員變數

private RecyclerListener mRecyclerListener;
//第一個可見的View儲存的位置
private int mFirstActivePosition;
//可見的View陣列
private View[] mActiveViews = new View[0];
//不可見的的View陣列,是一個集合陣列,每一種type的item都有一個集合來快取
private ArrayList<View>[] mScrapViews;
//View的Type的數量
private int mViewTypeCount;
//viewType為1的集合或者說mScrapViews的第一個元素
private ArrayList<View> mCurrentScrap;
複製程式碼

還有三個成員變數,沒有給出,因為涉及到StableId以Transient State這兩種屬性,下面解釋一下

  • StableId:就是所有的Item具有相同的ID,也就是所有的Item都相同,通過複寫BaseAdapter中的hasStableIds可以進行設定,預設為false
  • Transient State:在這裡面有一個Transient State,是View的一個屬性,說的是View伴隨有動畫之類的效果,對於這種狀態的View只有跟Adapter繫結的資料來源沒有發生變化或者View有相同的ID的時候才能進行快取複用,因為這兩種情況下Item要麼資料不變,不用重新繫結資料,要麼View不變,不需要重新建立
//資料集不變的具有TransientState的View陣列
private SparseArray<View> mTransientStateViews;
//具有固定ID的具有TransientState的View陣列
private LongSparseArray<View> mTransientStateViewsById;
//具有TransientState狀態但是不滿足以上任意一種狀態的View陣列,不予快取
private ArrayList<View> mSkippedScrap;
複製程式碼

初始化

setViewTypeCount

public void setViewTypeCount(int viewTypeCount) {
    if (viewTypeCount < 1) {
        throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
    }
    //根據viewTypeCount初始化陣列
    ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
    for (int i = 0; i < viewTypeCount; i++) {
        scrapViews[i] = new ArrayList<View>();
    }
    //初始化RecycleBin的陣列
    mViewTypeCount = viewTypeCount;
    mCurrentScrap = scrapViews[0];
    mScrapViews = scrapViews;

複製程式碼

fillActiveViews

存放螢幕上活躍的View陣列

void fillActiveViews(int childCount, int firstActivePosition) {
   //檢查ActiveView是否需要擴容
    if (mActiveViews.length < childCount) {
        mActiveViews = new View[childCount];
    }
    mFirstActivePosition = firstActivePosition;
    final View[] activeViews = mActiveViews;
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
        // Don't put header or footer views into the scrap heap
        if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
           //對用非Header以及Footer的child給activeViews陣列
            activeViews[i] = child;
            // 設定position的偏移量,方便接下來的佈局
            lp.scrappedFromPosition = firstActivePosition + i;
        }
    }
}
複製程式碼

addScrapView

對不在螢幕中的View進行快取

void addScrapView(View scrap, int position) {
   final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
    if (lp == null) {
        // Can't recycle, but we don't know anything about the view.
        // Ignore it completely.
        return;
    }
    //設定View的位置偏移量
    lp.scrappedFromPosition = position;
    final int viewType = lp.viewType;
   	//只要Type大於0就會進行快取
    if (!shouldRecycleViewType(viewType)) {
      //嘗試對非Header以及Footer的View進行快取
        if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            getSkippedScrap().add(scrap);
        }
        return;
    }
    scrap.dispatchStartTemporaryDetach();
    notifyViewAccessibilityStateChangedIfNeeded(
            AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
    // 不直接快取具有transient state的View,用transient陣列進行快取
    final boolean scrapHasTransientState = scrap.hasTransientState();
    if (scrapHasTransientState) {
        if (mAdapter != null && mAdapterHasStableIds) {
           //具有stableId
            if (mTransientStateViewsById == null) {
                mTransientStateViewsById = new LongSparseArray<>();
            }
           //存放進mTransientStateViewsById陣列
            mTransientStateViewsById.put(lp.itemId, scrap);
        } else if (!mDataChanged) {
           //資料集合沒有改變,暫時沒想到使用場景
            if (mTransientStateViews == null) {
                mTransientStateViews = new SparseArray<>();
            }
          //存放進mTransientStateViews陣列
            mTransientStateViews.put(position, scrap);
        } else {
            // 除此之外,不予快取,放入mSkippedScrap過濾陣列
            getSkippedScrap().add(scrap);
        }
    } else {
      //非transient state陣列,
        if (mViewTypeCount == 1) {
          //只有一種type,直接新增進mCurrentScrap
            mCurrentScrap.add(scrap);
        } else {
          //多type,則存放進對應type的陣列
            mScrapViews[viewType].add(scrap);
        }
		//回撥函式,通知View已經移動到Scrap進行快取
        if (mRecyclerListener != null) {
            mRecyclerListener.onMovedToScrapHeap(scrap);
        }
    }
}

複製程式碼

reclaimViews

開闢新集合,儲存所有的View包含ActivieView以及ScrapView

public void reclaimViews(List<View> views) {
    int childCount = getChildCount();
    RecyclerListener listener = mRecycler.mRecyclerListener;
    // Reclaim views on screen
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
        // Don't reclaim header or footer views, or views that should be ignored
        if (lp != null && mRecycler.shouldRecycleViewType(lp.viewType)) {
            views.add(child);
            child.setAccessibilityDelegate(null);
            if (listener != null) {
                // Pretend they went through the scrap heap
                listener.onMovedToScrapHeap(child);
            }
        }
    }
   //新增快取的View
    mRecycler.reclaimScrapViews(views);
    removeAllViewsInLayout();
}
複製程式碼

getScrapView

獲取快取的View

View getScrapView(int position) {
   //拿到當前位置View的type
    final int whichScrap = mAdapter.getItemViewType(position);
    if (whichScrap < 0) {
        return null;
    }
    if (mViewTypeCount == 1) {
       //type的種類為1,直接從第一個陣列中取
        return retrieveFromScrap(mCurrentScrap, position);
    } else if (whichScrap < mScrapViews.length) {
      //直接從對應的type陣列中取
        return retrieveFromScrap(mScrapViews[whichScrap], position);
    }
    return null;
}
複製程式碼

getActiveView

根據位置獲取螢幕中顯示的某一個陣列

   View getActiveView(int position) {
            int index = position - mFirstActivePosition;
            final View[] activeViews = mActiveViews;
            if (index >=0 && index < activeViews.length) {
                final View match = activeViews[index];
              //獲取之後將陣列置空,便於虛擬機器回收
                activeViews[index] = null;
                return match;
            }
            return null;
        }
複製程式碼

getTransientStateView

根據position獲取TransientStateView,獲取後會移除相應的View

View getTransientStateView(int position) {
    if (mAdapter != null && mAdapterHasStableIds && mTransientStateViewsById != null) {
        long id = mAdapter.getItemId(position);
        View result = mTransientStateViewsById.get(id);
        mTransientStateViewsById.remove(id);
        return result;
    }
    if (mTransientStateViews != null) {
        final int index = mTransientStateViews.indexOfKey(position);
        if (index >= 0) {
            View result = mTransientStateViews.valueAt(index);
            mTransientStateViews.removeAt(index);
            return result;
        }
    }
    return null;
}
複製程式碼

getSkippedScrap

獲取過濾掉也是不快取的陣列

private ArrayList<View> getSkippedScrap() {
    if (mSkippedScrap == null) {
        mSkippedScrap = new ArrayList<>();
    }
    return mSkippedScrap;
}
複製程式碼

retrieveFromScrap

從一個type對應的快取集合中尋找指定位置的View

private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
    final int size = scrapViews.size();
    if (size > 0) {
        // See if we still have a view for this position or ID.
        for (int i = 0; i < size; i++) {
            final View view = scrapViews.get(i);
            final AbsListView.LayoutParams params =
             (AbsListView.LayoutParams) view.getLayoutParams();
            if (mAdapterHasStableIds) {
                final long id = mAdapter.getItemId(position);
                if (id == params.itemId) {
                    return scrapViews.remove(i);
                }
            } else if (params.scrappedFromPosition == position) {
                final View scrap = scrapViews.remove(i);
                clearAccessibilityFromScrap(scrap);
                return scrap;
            }
        }
        final View scrap = scrapViews.remove(size - 1);
        clearAccessibilityFromScrap(scrap);
        return scrap;
    } else {
        return null;
    }
}
複製程式碼

清除

clearTransientStateViews

清空TransientStateViews

void clearTransientStateViews() {
    final SparseArray<View> viewsByPos = mTransientStateViews;
    if (viewsByPos != null) {
        final int N = viewsByPos.size();
        for (int i = 0; i < N; i++) {
            removeDetachedView(viewsByPos.valueAt(i), false);
        }
        viewsByPos.clear();
    }

    final LongSparseArray<View> viewsById = mTransientStateViewsById;
    if (viewsById != null) {
        final int N = viewsById.size();
        for (int i = 0; i < N; i++) {
            removeDetachedView(viewsById.valueAt(i), false);
        }
        viewsById.clear();
    }
}
複製程式碼

removeSkippedScrap

刪除過濾掉的陣列

void removeSkippedScrap() {
    if (mSkippedScrap == null) {
        return;
    }
    final int count = mSkippedScrap.size();
    for (int i = 0; i < count; i++) {
        removeDetachedView(mSkippedScrap.get(i), false);
    }
    mSkippedScrap.clear();
}
複製程式碼

clearScrap

清空指定的Scrap集合

private void clearScrap(final ArrayList<View> scrap) {
    final int scrapCount = scrap.size();
    for (int j = 0; j < scrapCount; j++) {
        removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
    }
}
複製程式碼

clear

清空所有快取陣列

void clear() {
    if (mViewTypeCount == 1) {
        final ArrayList<View> scrap = mCurrentScrap;
        clearScrap(scrap);
    } else {
        final int typeCount = mViewTypeCount;
        for (int i = 0; i < typeCount; i++) {
            final ArrayList<View> scrap = mScrapViews[i];
            clearScrap(scrap);
        }
    }
    clearTransientStateViews();
}
複製程式碼

pruneScrapViews

當快取的陣列大於Activite陣列之後就需要進行清理了,主要是因為transient state View陣列中View的transient屬性已經消失了,所以相當於會被快取兩邊,一遍是具有transient state的View,一邊是這些View不再具有transient state的時候又會進行快取一次,這樣就會導致快取中的陣列大於Active陣列,所以進行處理。

private void pruneScrapViews() {
    final int maxViews = mActiveViews.length;
    final int viewTypeCount = mViewTypeCount;
    final ArrayList<View>[] scrapViews = mScrapViews;
   //Scrap陣列遍歷
    for (int i = 0; i < viewTypeCount; ++i) {
        final ArrayList<View> scrapPile = scrapViews[i];
        int size = scrapPile.size();
        while (size > maxViews) {
            scrapPile.remove(--size);
        }
    }
    //transViewsByPos遍歷
    final SparseArray<View> transViewsByPos = mTransientStateViews;
    if (transViewsByPos != null) {
        for (int i = 0; i < transViewsByPos.size(); i++) {
            final View v = transViewsByPos.valueAt(i);
            if (!v.hasTransientState()) {
                removeDetachedView(v, false);
                transViewsByPos.removeAt(i);
                i--;
            }
        }
    }
	//mTransientStateViewsById遍歷
    final LongSparseArray<View> transViewsById = mTransientStateViewsById;
    if (transViewsById != null) {
        for (int i = 0; i < transViewsById.size(); i++) {
            final View v = transViewsById.valueAt(i);
            if (!v.hasTransientState()) {
                removeDetachedView(v, false);
                transViewsById.removeAt(i);
                i--;
            }
        }
    }
}
複製程式碼

markChildrenDirty

此方法會重新呼叫快取的child的forcelayout,forcelayout跟requestLayout的區別在於前者只會重新執行自己的onMeasure跟onLayout,後者不僅僅會執行前者,還會呼叫parentView的requestLayout,此方法會在ListView的size改變的時候進行呼叫。

public void markChildrenDirty() {
    if (mViewTypeCount == 1) {
        final ArrayList<View> scrap = mCurrentScrap;
        final int scrapCount = scrap.size();
        for (int i = 0; i < scrapCount; i++) {
            scrap.get(i).forceLayout();
        }
    } else {
        final int typeCount = mViewTypeCount;
        for (int i = 0; i < typeCount; i++) {
            final ArrayList<View> scrap = mScrapViews[i];
            final int scrapCount = scrap.size();
            for (int j = 0; j < scrapCount; j++) {
                scrap.get(j).forceLayout();
            }
        }
    }
    if (mTransientStateViews != null) {
        final int count = mTransientStateViews.size();
        for (int i = 0; i < count; i++) {
            mTransientStateViews.valueAt(i).forceLayout();
        }
    }
    if (mTransientStateViewsById != null) {
        final int count = mTransientStateViewsById.size();
        for (int i = 0; i < count; i++) {
            mTransientStateViewsById.valueAt(i).forceLayout();
        }
    }
}
複製程式碼

上面詳細分了RecyclerBin的成員變數以及生命週期,內部通過定義了多個陣列來對不同型別的View進行快取,那麼AbsListView的子類也就是ListView以及GridView滾動,以及呼叫notifyDataSetChanged的時候,然後進行重新繪製,先從快取中取如果沒有取到則會重新建立一個View,關於這方面的分析網上已經有很多文章了,只是很多文章沒有對RecycleBin的TranslateStateView陣列進行分析,自己之前也是一直沒有完全理解,所以重點分析了RecycleBin這個類,至於ListView以及GridView的繪製以及重新整理機制,其實相對於RecyclerView來講其實是比較簡單的,所以大部分精力也將用來分析RecyclerView的快取機制

RecyclerView

註釋

A flexible view for providing a limited window into a large data set
複製程式碼

能夠在有限的視窗內展示大量資料集合的一個靈活的View。

相對於AbsListView的兩個子類ListView以及GridView來講,RecyclerView最大一個特性就是靈活,主要體現在以下兩個方面

  • 多樣式:可以對資料的展示進行自有定製,可以是列表,網格甚至是瀑布流,除此之外你還可以自定義樣式
  • 定向重新整理:可以對指定的Item資料進行重新整理
  • 重新整理動畫:RecyclerView支援對Item的重新整理新增動畫
  • 新增裝飾:相對於ListView以及GridView的單一的分割線,RecyclerView可以自定義新增分割樣式

今天我們的重點在於快取,所以重點研究定向重新整理,除了對整體資料進行重新整理之外,RecyclerView還提供了很多定向重新整理的方法。

notifData

由於RecyclerView的很多核心功能都是通過內部類來實現的,而且作者又是把內部類寫在一個類裡面,所以還是要先對每個類的功能進行簡單介紹一下,整個RecyclerView的原始碼有1萬多行,不可能也沒必要面面俱到,還是有針對性地進行分析,不然看原始碼絕對會走火入魔。

RecyclerView

他們的作用如下表

內部類
RecyclerView.LayoutManager 負責Item檢視的佈局的顯示管理
RecyclerView.ItemDecoration 給每一項Item檢視新增修飾的View,
RecyclerView.Adapter 為每一項Item建立檢視
RecyclerView.ViewHolder 承載Item檢視的子佈局
RecyclerView.ItemAnimator 負責處理資料新增或者刪除時候的動畫效果
RecyclerView.Cache Recycler/RecycledViewPool/ViewCacheExtension

最後一個Cache是我加上去的,實際上並沒有這個類,主要是為了說明RecyclerView對Item強大的快取,也是接下來重點分析的物件

Observer&Observable

Observer

Recycler沒有采用系統的Observer,因為自己需要接收通知的方法過多,所以自己設計了一個觀察者AdapterDataObserver,先看一下繼承關係

AdapterDataObserver

AdapterDataObserver

public static abstract class AdapterDataObserver {
    public void onChanged() {
        // Do nothing
    }

    public void onItemRangeChanged(int positionStart, int itemCount) {
        // do nothing
    }

    public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
        onItemRangeChanged(positionStart, itemCount);
    }

    public void onItemRangeInserted(int positionStart, int itemCount) {
        // do nothing
    }

    public void onItemRangeRemoved(int positionStart, int itemCount) {
        // do nothing
    }

    public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
        // do nothing
    }
}
複製程式碼

RecyclerViewDataObserver

private class RecyclerViewDataObserver extends AdapterDataObserver {
    RecyclerViewDataObserver() {
    }
    @Override
    public void onChanged() {
        assertNotInLayoutOrScroll(null);
        mState.mStructureChanged = true;

        setDataSetChangedAfterLayout();
        if (!mAdapterHelper.hasPendingUpdates()) {
            requestLayout();
        }
    }

    @Override
    public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
        assertNotInLayoutOrScroll(null);
        if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
            triggerUpdateProcessor();
        }
    }

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

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

    @Override
    public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
        assertNotInLayoutOrScroll(null);
        if (mAdapterHelper.onItemRangeMoved(fromPosition, toPosition, itemCount)) {
            triggerUpdateProcessor();
        }
    }

    void triggerUpdateProcessor() {
        if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
            ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
        } else {
            mAdapterUpdateDuringMeasure = true;
            requestLayout();
        }
    }
}

複製程式碼

這裡面在notify的方法裡面,提到了一個類mAdapterHelper,顧名思義是Adapter的幫助類,他是AdapterHelper的例項,用來幫助Adapter進行資料更新,除此之外還有幾個類也是相對比較重要的,下面會簡單介紹一下,因為RecyclerView的原始碼有1萬多行,作者幾乎是把所有與RecyclerView相關的類搞成了內部類,可能作者對內部類有一種特殊的感情,問題是你可以這樣子搞,但是程式碼能不能多加點註釋,很多方法完全沒註釋,看起來真心一臉懵逼的類。

Observable

RecyclerView實際採用了系統的Observable,看一下繼承關係

AdapterDataObservable

Observable

程式碼比較簡單,不忍註釋

public abstract class Observable<T> {
 	//註冊的觀察者
    protected final ArrayList<T> mObservers = new ArrayList<T>();
  //註冊單個觀察者
    public void registerObserver(T observer) {
        if (observer == null) {
            throw new IllegalArgumentException("The observer is null.");
        }
        synchronized(mObservers) {
            if (mObservers.contains(observer)) {
                throw new IllegalStateException("Observer " + observer + " is already registered.");
            }
            mObservers.add(observer);
        }
    }
  //解綁單個觀察者
    public void unregisterObserver(T observer) {
        if (observer == null) {
            throw new IllegalArgumentException("The observer is null.");
        }
        synchronized(mObservers) {
            int index = mObservers.indexOf(observer);
            if (index == -1) {
      throw new IllegalStateException("Observer " + observer + " was not registered.");
            }
            mObservers.remove(index);
        }
    }

    //移除所有的觀察者
    public void unregisterAll() {
        synchronized(mObservers) {
            mObservers.clear();
        }
    }
}
複製程式碼

AdapterDataObservable

比較簡單,懶得註釋,寫出來是便於梳理整個流程

static class AdapterDataObservable extends Observable<AdapterDataObserver> {
    public boolean hasObservers() {
        return !mObservers.isEmpty();
    }

    public void notifyChanged() {
        for (int i = mObservers.size() - 1; i >= 0; i--) {
            mObservers.get(i).onChanged();
        }
    }

    public void notifyItemRangeChanged(int positionStart, int itemCount) {
        notifyItemRangeChanged(positionStart, itemCount, null);
    }

    public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
        for (int i = mObservers.size() - 1; i >= 0; i--) {
            mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
        }
    }

    public void notifyItemRangeInserted(int positionStart, int itemCount) {
        for (int i = mObservers.size() - 1; i >= 0; i--) {
            mObservers.get(i).onItemRangeInserted(positionStart, itemCount);
        }
    }

    public void notifyItemRangeRemoved(int positionStart, int itemCount) {
        for (int i = mObservers.size() - 1; i >= 0; i--) {
            mObservers.get(i).onItemRangeRemoved(positionStart, itemCount);
        }
    }
    public void notifyItemMoved(int fromPosition, int toPosition) {
        for (int i = mObservers.size() - 1; i >= 0; i--) {
            mObservers.get(i).onItemRangeMoved(fromPosition, toPosition, 1);
        }
    }
}

複製程式碼

看完了RecyclerView的觀察者,感覺實際上根本不需要繼承系統的被觀察者以及自定義觀察者,因為只有一個觀察者,所以乾脆使用介面回撥,口味更佳,下面繼續分析RecyclerView快取的核心類。

Recycler

在Recycler中實際上快取VieHolder的有2類集合,一類是可見的ViewHolder陣列,一類是不可見的ViewHolder陣列,其中可見的陣列中又分為資料改變跟沒有改變的,理解了這些,其實去看

註釋

 A Recycler is responsible for managing scrapped or detached item views for reuse.
 A "scrapped" view is a view that is still attached to its parent RecyclerView but
 that has been marked for removal or reuse.
 
複製程式碼

Recycler是用於管理廢棄的item或者從RecyclerView中移除的View便於複用,廢棄的View是指仍然依附在RecyclerView中,但是已經被標記為移除的或者可以複用的。

跟ListView的RecycleBin一樣,Recycler也是RecyclerView設計的一個專門用於回收ViewHolder的類,其實RecyclerView的快取機制是在ListView的快取機制的基礎上進一步的完善,所以在Recycler中能看到很多跟RecycleBin一樣的設計思想,在快取這個層面上,RecyclerView實際上並沒有做出太大的創新,最大的創新來源於給每一個ViewHolder增加了一個UpdateOp,通過這個標誌可以進行定向重新整理指定的Item,並且通過Payload引數可以對Item進行區域性重新整理,我覺得這個是RecyclerView最厲害的地方,大大提高了重新整理時候的效能,如果資料來源需要經常變動,那麼RecyclerView是你最好的選擇,沒有之一,下面看一下Recycler是如何進行快取的。

成員變數

static final int DEFAULT_CACHE_SIZE = 2;//預設快取的數量
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;//設定的快取最大數量,預設為2
int mViewCacheMax = DEFAULT_CACHE_SIZE;//View的快取的最大數量,預設為2
RecycledViewPool mRecyclerPool;//RecycledView,用來公用RecyclerView
//快取的擴充套件,可以用來對指定的position跟Type進行快取
private ViewCacheExtension mViewCacheExtension;
ArrayList<ViewHolder> mChangedScrap = null;//資料來源更改過的AttachedScrap
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();//依附的快取View
//快取的全部View,包含可見跟不可見的ViewHolder
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder> mUnmodifiableAttachedScrap  = Collections.unmodifiableList(mAttachedScrap);//mAttachedScrap的不可變集合

複製程式碼

初始化

我們發現在成員變數中沒有初始化的只有兩個變數mChangedScrap,mRecyclerPool,mChangedScrap是可見的資料來源改變的ViewHolder集合,mRecyclerPool是RecycledViewPool的集合,用來快取RecyclerView的集合,可以實現RecyclerView的複用。

mChangedScrap是在scrapView這個方法進行初始化的

 if (mChangedScrap == null) {
 mChangedScrap = new ArrayList<ViewHolder>();
        }
複製程式碼

RecycledViewPool初始化

void setRecycledViewPool(RecycledViewPool pool) {
    if (mRecyclerPool != null) {
        mRecyclerPool.detach();
    }
    mRecyclerPool = pool;
    if (pool != null) {
        mRecyclerPool.attach(getAdapter());
    }
}
複製程式碼

recycleView

回收不可見的View,對於某些特定的View會放進RecyclerViewPool,如果這個ViewHolder是從快取中取的,那麼就會清空快取中View

public void recycleView(View view) {
   //這邊傳遞過來的是一個View,然後通過View獲取ViewHolder
    ViewHolder holder = getChildViewHolderInt(view);
   //如果Holder被已經被打上了移除的標記,那麼就從移除此View
    if (holder.isTmpDetached()) {
        removeDetachedView(view, false);
    }
  
    if (holder.isScrap()) {
      //如果此Holder是來自快取的可見的ViewHolder陣列,清楚快取
        holder.unScrap();
    } else if (holder.wasReturnedFromScrap()) {
      //如果此Holder是來自快取的不可見的ViewHolder陣列,清除快取
        holder.clearReturnedFromScrapFlag();
    }
   //開始快取,繼續追原始碼
    recycleViewHolderInternal(holder);
}
複製程式碼

recycleViewHolderInternal

void recycleViewHolderInternal(ViewHolder holder) {
  	//此處省略若干行條件判斷程式碼
    if (forceRecycle || holder.isRecyclable()) {
        //如果快取數量大於0,並且ViewHolder的Flag標誌是有效的
       //且非REMOVED跟UPDATE,進行快取,
        if (mViewCacheMax > 0&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                | ViewHolder.FLAG_REMOVED| ViewHolder.FLAG_UPDATE
                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
              // 移除cachedViews中的第一個View,也就是第一個
                recycleCachedViewAt(0);
                cachedViewSize--;
            }
			//獲取新增元素的在快取集合中的下標
            int targetCacheIndex = cachedViewSize;
            if (ALLOW_THREAD_GAP_WORK&& cachedViewSize > 0
              && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                int cacheIndex = cachedViewSize - 1;
                while (cacheIndex >= 0) {
                    int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                    //快取的時候不能覆蓋最近經常被使用到快取
                    if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                        break;
                    }
                    cacheIndex--;
                }
                targetCacheIndex = cacheIndex + 1;
            }
            //新增快取
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
      //如果沒有快取的話就新增進RecycledViewPool
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } else {
        if (DEBUG) {
            Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
                    + "re-visit here. We are still removing it from animation lists"
                    + exceptionLabel());
        }
    }
   //mViewInfoStore中移除這條記錄
    mViewInfoStore.removeViewHolder(holder);
    if (!cached && !recycled && transientStatePreventsRecycling) {
        holder.mOwnerRecyclerView = null;
    }
}

複製程式碼

Recycler的快取做了很多優化,實際上也採用了LFU演算法,也就是最少使用策略,當我們儲存ViewHolder的時候,會去判斷這個ViewHolder是否是來自快取,如果是的話,那麼在此快取的時候不能覆蓋最近使用比較頻繁的快取,而是自己定義了一個類GapWorker,他有一個內部類LayoutPrefetchRegistryImpl,然後定義了一個陣列

int[] mPrefetchArray;
複製程式碼

這個陣列是用來記錄最近使用過的快取Holder,所以我們在存的時候會跟這裡面儲存過的ViewHolder進行匹配,上面程式碼中已經進行了註釋,比較好理解。

recycleAndClearCachedViews

將CacheViews中的ViewHolder新增進RecyclerViewHolder,然後清空CacheViews

void recycleAndClearCachedViews() {
    final int count = mCachedViews.size();
    for (int i = count - 1; i >= 0; i--) {
    //將CacheView中的新增進RecyclerViewPool
        recycleCachedViewAt(i);
    }
    //清空集合
    mCachedViews.clear();
    if (ALLOW_THREAD_GAP_WORK) {
        mPrefetchRegistry.clearPrefetchPositions();
    }
}

複製程式碼

recycleCachedViewAt

void recycleCachedViewAt(int cachedViewIndex) {
    //省略一下判斷程式碼
    ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
    //新增進ToRecycledViewPool
    addViewHolderToRecycledViewPool(viewHolder, true);
    //從mCachedViews移除ViewHolder
    mCachedViews.remove(cachedViewIndex);
}
複製程式碼

addViewHolderToRecycledViewPool

void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
    clearNestedRecyclerViewIfNotNested(holder);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
        holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
        ViewCompat.setAccessibilityDelegate(holder.itemView, null);
    }
    if (dispatchRecycled) {
        dispatchViewRecycled(holder);
    }
    holder.mOwnerRecyclerView = null;
   //放入RecyclerViewPool
    getRecycledViewPool().putRecycledView(holder);
}
複製程式碼

取快取有很多入口,但是最終都是呼叫了tryGetViewHolderForPositionByDeadline,下面重點分析一下此方法

tryGetViewHolderForPositionByDeadline

   /**
     * @param position Position of ViewHolder to be returned.
     * @param dryRun True if the ViewHolder should not be removed from scrap/cache/
     * @param deadlineNs Time, relative to getNanoTime(), by which bind/create work 			should complete. If FOREVER_NS is passed, this method will not fail to
        create/bind the holder if needed.
    */
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(
  int position,boolean dryRun, long deadlineNs) {
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // 0) 首先從Attached中的Changed陣列中取
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    // 1) 分別從AttachedScrap,Hidden,Cached中獲取ViewHolder
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
            if (!validateViewHolderForOffsetPosition(holder)) {
                if (!dryRun) {
                    holder.addFlags(ViewHolder.FLAG_INVALID);
                    if (holder.isScrap()) {
                        removeDetachedView(holder.itemView, false);
                        holder.unScrap();
                    } else if (holder.wasReturnedFromScrap()) {
                        holder.clearReturnedFromScrapFlag();
                    }
                    recycleViewHolderInternal(holder);
                }
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2)通過StableId進行獲取,針對複寫了BaseAdapter的StableId
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
            }
        }
       //  3)通過mViewCacheExtension進行獲取
        if (holder == null && mViewCacheExtension != null) {
            final View view = mViewCacheExtension
                   .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
		//省略Holder的判斷邏輯
        }
        //  4)無路可退,通過RecyclerViewPool進行獲取
        if (holder == null) { 
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
       //  5)如果上面都沒有獲取到,那麼就說明是第一屏,所以就得重新建立
        if (holder == null) {
            long start = getNanoTime();
          //省略判空程式碼
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            if (ALLOW_THREAD_GAP_WORK) {
                // 放入最近最少使用的佇列中
                RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                if (innerView != null) {
                    holder.mNestedRecyclerView = new WeakReference<>(innerView);
                }
            }
            long end = getNanoTime();
            mRecyclerPool.factorInCreateTime(type, end - start);
        }
    }

    //在返回給LayoutManager重新繪製前,需要更新一下ViewHolder的相關資訊
    if (fromScrapOrHiddenOrCache && !mState.isPreLayout() 
        && holder.hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
        holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
      //記錄動畫資訊
        if (mState.mRunSimpleAnimations) {
            int changeFlags = ItemAnimator.buildAdapterChangeFlagsForAnimations(holder);
            changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
            final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(
              mState, holder, changeFlags, holder.getUnmodifiedPayloads());
          //如果當前的ViewHolder已經繫結過資料,那麼記錄一下動畫資訊
            recordAnimationInfoIfBouncedHiddenView(holder, info);
        }
    }

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
      //繫結過資料的ViewHolder
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
      //未繫結資料的ViewHolder需要進行資料繫結
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    // 將ViewHolder設定給ViewGroup.LayoutParams
    final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    final LayoutParams rvLayoutParams;
    if (lp == null) {
        rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else if (!checkLayoutParams(lp)) {
        rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else {
        rvLayoutParams = (LayoutParams) lp;
    }
    rvLayoutParams.mViewHolder = holder;
    rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
    return holder;
}
複製程式碼

將AttachedView進行快取

void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
      //資料來源未改變,放入mAttachedScrap
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
     //省略部分邏輯判斷程式碼
        holder.setScrapContainer(this, false);
        mAttachedScrap.add(holder);
    } else {
       //資料來源未改變,放入mAttachedScrap
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}
複製程式碼

清空

clear

將CachedView新增進ViewHolder,並且清空

public void clear() {
    mAttachedScrap.clear();
    recycleAndClearCachedViews();
}
複製程式碼

clearScrap

清空快取中可見的Scrap陣列

void clearScrap() {
    mAttachedScrap.clear();
    if (mChangedScrap != null) {
        mChangedScrap.clear();
    }
}
複製程式碼

定向重新整理

可以對指定的Item進行重新整理,是RecyclerView的又一特點,RecyclerView在觀察者模式中的Observer中新增了很多方法,用於定向重新整理,這個在前面的觀察者模式中已經提到過,下面從原始碼的角度分析一下原理,我們拿adapter.notifyItemChanged(0),最終會RecyclerViewDataObserver的方法

onItemRangeChanged

@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
    assertNotInLayoutOrScroll(null);
    //繼續追蹤mAdapterHelper
    if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
        triggerUpdateProcessor();
    }
}
複製程式碼

首先呼叫了AdapterHelper的onItemRangeChanged方法,這裡簡單說一下AdapterHelper,用來幫助Adapter更新陣列的,類似於ChildHelper幫助LayoutManager進行佈局

boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
    if (itemCount < 1) {
        return false;
    }
  //將需要更新的Item的範圍記錄下來,並新增更新標識
    mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
    mExistingUpdateTypes |= UpdateOp.UPDATE;
    return mPendingUpdates.size() == 1;
}
複製程式碼

然後接著呼叫了triggerUpdateProcessor方法

void triggerUpdateProcessor() {
    if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
      //如果有動畫,先執行動畫,然後再進行測量
        ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
    } else {
        mAdapterUpdateDuringMeasure = true;
       //重新測量
        requestLayout();
    }
}
複製程式碼

其實看到這裡,發現AdapterHelper只是記錄了需要重新整理的Item的範圍,然後就開始進行重新測量了,那麼很明顯,不管是全部重新整理還是指定範圍的定向重新整理,RecyclerView都是需要重新測量的,所以,定向重新整理的真正語義是對指定範圍的ViewHolder進行資料重新整理,不會像ListView一樣,重新整理所有的,所以我們只能從佈局的時候來查詢,RecyclerView的佈局是交給LayoutMananger來進行佈局的,那麼根據AbsListView一樣,LayoutMananger一定會在自身當中定義一些公共方法給子類實現,這個方法實際上就是onLayoutChildren,還有onLayoutCompleted,是在繪製完成的時候進行呼叫,進行資源清理

public void onLayoutChildren(Recycler recycler, State state) {
    Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}
//做一些清理工作
 public void onLayoutCompleted(State state) {
        }
複製程式碼

我們繼續檢視子類中的實現

onLayoutChildren

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
  // layout algorithm:
  // 1) by checking children and other variables, find an anchor coordinate and an anchor
  //  item position.
  // 2) fill towards start, stacking from bottom
  // 3) fill towards end, stacking from top
  // 4) scroll to fulfill requirements like stack from bottom.
   //開始佈局
   fill(recycler, mLayoutState, state, false);
}
  
複製程式碼

繼續追蹤fill方法

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    // max offset we should set is mFastScroll + available
    final int start = layoutState.mAvailable;
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        // TODO ugly bug fix. should not happen
        if (layoutState.mAvailable < 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        recycleByLayoutState(recycler, layoutState);
    }
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        layoutChunkResult.resetInternal();
        if (VERBOSE_TRACING) {
            TraceCompat.beginSection("LLM LayoutChunk");
        }
        //迴圈中呼叫了此方法
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        if (VERBOSE_TRACING) {
            TraceCompat.endSection();
        }
        if (layoutChunkResult.mFinished) {
            break;
        }
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
   
        if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
                || !state.isPreLayout()) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            // we keep a separate remaining space because mAvailable is important for recycling
            remainingSpace -= layoutChunkResult.mConsumed;
        }

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

繼續追蹤layoutChunk

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    View view = layoutState.next(recycler);
    if (view == null) {
        if (DEBUG && layoutState.mScrapList == null) {
            throw new RuntimeException("received null view when unexpected");
        }
        result.mFinished = true;
        return;
    }
    LayoutParams params = (LayoutParams) view.getLayoutParams();
    if (layoutState.mScrapList == null) {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            addView(view);
        } else {
            addView(view, 0);
        }
    } else {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            addDisappearingView(view);
        } else {
            addDisappearingView(view, 0);
        }
    }
    measureChildWithMargins(view, 0, 0);
    result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
    int left, top, right, bottom;
    if (mOrientation == VERTICAL) {
        if (isLayoutRTL()) {
            right = getWidth() - getPaddingRight();
            left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
        } else {
            left = getPaddingLeft();
            right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
        }
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            bottom = layoutState.mOffset;
            top = layoutState.mOffset - result.mConsumed;
        } else {
            top = layoutState.mOffset;
            bottom = layoutState.mOffset + result.mConsumed;
        }
    } else {
        top = getPaddingTop();
        bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);

        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            right = layoutState.mOffset;
            left = layoutState.mOffset - result.mConsumed;
        } else {
            left = layoutState.mOffset;
            right = layoutState.mOffset + result.mConsumed;
        }
    }
 
    layoutDecoratedWithMargins(view, left, top, right, bottom);
    if (params.isItemRemoved() || params.isItemChanged()) {
        result.mIgnoreConsumed = true;
    }
    result.mFocusable = view.hasFocusable();
}
複製程式碼

我看了一遍,這裡面並沒有給出是否BindViewHolder的方法,所以最開始的思路錯了,應該是在layoutChildren呼叫之前就已經確定是否繫結,然後我們發現layoutChildren是在dispatchLayoutStep1/2/3中都有呼叫,這個就尷尬了,不過我們繼續檢視此方法,發現不僅僅這三個方法在會在dispatchLayout中被呼叫到,

void dispatchLayout() {
    if (mAdapter == null) {
        Log.e(TAG, "No adapter attached; skipping layout");
        // leave the state in START
        return;
    }
    if (mLayout == null) {
        Log.e(TAG, "No layout manager attached; skipping layout");
        // leave the state in START
        return;
    }
    mState.mIsMeasuring = false;
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() ||
            mLayout.getHeight() != getHeight()) {
        // First 2 steps are done in onMeasure but looks like we have to run again due to
        // changed size.
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else {
        // always make sure we sync them (to ensure mode is exact)
        mLayout.setExactMeasureSpecsFrom(this);
    }
    dispatchLayoutStep3();
}
複製程式碼

而dispatchLayout這個方法在onLayout中單獨進行呼叫,而且只呼叫了這一個方法,那麼沒有疑問,應該是在佈局的時候來判斷,所以先檢視一下dispatchLayoutStep這三兄弟

dispatchLayout

根據註釋,我們是在第一個方法裡面進行ViewHolder的更新,所以我們重點檢視一下dispatchLayoutStep1這個方法

dispatchLayoutStep1

private void dispatchLayoutStep1() {
    mState.assertLayoutStep(State.STEP_START);
    mState.mIsMeasuring = false;
    eatRequestLayout();
    mViewInfoStore.clear();
    onEnterLayoutOrScroll();
    saveFocusInfo();
    //命名太規範了,一下子就找到了,繼續追蹤
    processAdapterUpdatesAndSetAnimationFlags(); 
}

複製程式碼

processAdapterUpdatesAndSetAnimationFlags

private void processAdapterUpdatesAndSetAnimationFlags() {
    if (mDataSetHasChangedAfterLayout) {
        // Processing these items have no value since data set changed unexpectedly.
        // Instead, we just reset it.
        mAdapterHelper.reset();
        markKnownViewsInvalid();
      //最開始以為是在這個方法中進行回撥的,後來發現在子類是空實現,
      //也就是說這個方法供使用著自己定義,通知回撥
        mLayout.onItemsChanged(this);
    }
    if (predictiveItemAnimationsEnabled()) {
        mAdapterHelper.preProcess();
    } else {
      //更新ViewHolder
        mAdapterHelper.consumeUpdatesInOnePass();
    }
	//省略動畫相關操作
}
複製程式碼

consumeUpdatesInOnePass

void consumeUpdatesInOnePass() {
    consumePostponedUpdates();
    final int count = mPendingUpdates.size();
    for (int i = 0; i < count; i++) {
        UpdateOp op = mPendingUpdates.get(i);
        switch (op.cmd) {
         case UpdateOp.ADD:
          mCallback.onDispatchSecondPass(op);
          mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
                break;
         case UpdateOp.REMOVE:
           mCallback.onDispatchSecondPass(op);
           mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
                break;
          case UpdateOp.UPDATE:
              //介面回撥
           mCallback.onDispatchSecondPass(op);
            mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
                break;
            case UpdateOp.MOVE:
                mCallback.onDispatchSecondPass(op);
                mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
                break;
        }
        if (mOnItemProcessedCallback != null) {
            mOnItemProcessedCallback.run();
        }
    }
  //這裡也是回收,實際上這個回收不是迴圈利用,就是清空的意思
    recycleUpdateOpsAndClearList(mPendingUpdates);
    mExistingUpdateTypes = 0;
}
複製程式碼

這個介面是初始化的時候傳入的,我們追蹤此介面

@Override
public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) {
  //這個方法才是真正的更新Adapter的ViewHolder,繼續跟進
    viewRangeUpdate(positionStart, itemCount, payload);
    mItemsChanged = true;
}

@Override
public void onDispatchFirstPass(UpdateOp op) {
  //他的方法都是空實現,子類也沒有實現,我們可以複寫來接收通知
    dispatchUpdate(op);
}

void dispatchUpdate(UpdateOp op) {
    switch (op.cmd) {
        case UpdateOp.ADD:
            mLayout.onItemsAdded(RecyclerView.this, op.positionStart, op.itemCount);
            break;
        case UpdateOp.REMOVE:
            mLayout.onItemsRemoved(RecyclerView.this, op.positionStart, op.itemCount);
            break;
        case UpdateOp.UPDATE:
            mLayout.onItemsUpdated(RecyclerView.this, op.positionStart, op.itemCount,
                    op.payload);
            break;
        case UpdateOp.MOVE:
            mLayout.onItemsMoved(RecyclerView.this, op.positionStart, op.itemCount, 1);
            break;
    }
}
複製程式碼

viewRangeUpdate

通過名字也能很好的檢視出,進行範圍內的更新

void viewRangeUpdate(int positionStart, int itemCount, Object payload) {
    final int childCount = mChildHelper.getUnfilteredChildCount();
    final int positionEnd = positionStart + itemCount;

    for (int i = 0; i < childCount; i++) {
        final View child = mChildHelper.getUnfilteredChildAt(i);
        final ViewHolder holder = getChildViewHolderInt(child);
        if (holder == null || holder.shouldIgnore()) {
            continue;
        }
        if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
           //新增Flag,這個是從快取中獲取到ViewHolder的時候,需不需要重新整理的唯一標識
            holder.addFlags(ViewHolder.FLAG_UPDATE);
           //新增payLoad引數,用於ViewHolder的區域性重新整理
            holder.addChangePayload(payload);
            // lp cannot be null since we get ViewHolder from it.
            ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
        }
    }
  //更新快取中的標記位,便於資料來源不改變的時候直接複用
    mRecycler.viewRangeUpdate(positionStart, itemCount);
}
複製程式碼

區域性重新整理

在上面提到過,我們重新整理的時候會呼叫一個方法,叫做notifyItemChanged,通常會傳一個起始位置以及範圍,其實我們還可以傳入一個Object型別的引數,

public final void notifyItemChanged(int position, Object payload) {
    mObservable.notifyItemRangeChanged(position, 1, payload);
}
複製程式碼

如何獲取呢,也很簡單,複寫onBindViewHolder這個方法,

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) {
  //點進去會進入下面的方法
    super.onBindViewHolder(holder, position, payloads);
}
複製程式碼

預設的實現是onBindViewHolder,也就是我們經常複寫的方法

public void onBindViewHolder(VH holder, int position, List<Object> payloads) {
    onBindViewHolder(holder, position);
}
複製程式碼

這個引數有什麼用,就是用於區域性重新整理的,比如一個ViewHolder展示了很多關於學生的資料,姓名,年齡,愛好,學習成績,家庭住址等,但是我們只需要重新整理一下學習成績,我們在呼叫notifyItemChanged的時候只需要呼叫一下otifyItemChanged(0, score),然後在Adapter中進行獲取就好了重新整理這一項就好了。

DiffUtil

當我們知道我們要重新整理的Item的範圍的時候,可以直接呼叫notifyItemChanged(position,range),當我們更新前後的集合資料差不多,只有部分差異的時候,我們都還是呼叫的是notifyDataSetChanged,即全量重新整理,其實谷歌已經意識到這個問題,已經在support包中提供了一個工具類DiffUtil,可以幫助我們進行分析對比前後的資料差異,從而進行定向重新整理,我們來分析一下這個工具類:

註釋

DiffUtil is a utility class that can calculate the difference between two lists and output a list of update operations that converts the first list into the second one.
複製程式碼

Diffutil是一個可以計算出兩個集合之間的差別並且輸出從一個集合到另外一個集合之間的變化的工具類

從註釋可以看出,DifUtil的主要操作主要是用來比對兩個新舊集合之間產生的差異,同時DiffUtil底層採用的是Myers差分演算法,大家可以先看看這篇文章瞭解一下Myers差分演算法,不然下面的肯定很不好理解,說通俗一點就現在有新舊兩個集合,長度分別為M、N,是通過構造一個平面直角座標系,然後X軸向右,用原資料集合中的元素填充橫座標,Y軸向下,新資料集合中的元素填充縱座標,

diff

然後需要計算從座標(0,0)到(M,N)的最短路徑,圖中的對角線表示元素相同,如果走對角線的話不算在總長度內,因為沒有出現差分,說白了也就是隻要兩個集合中有相同的元素,那麼就會在舊集合中進行刪除或者新增操作,從而最大限度的保證從舊集合到新集合的定向重新整理。

成員變數

private static final Comparator<Snake> SNAKE_COMPARATOR = new Comparator<Snake>() {
    @Override
    public int compare(Snake o1, Snake o2) {
        int cmpX = o1.x - o2.x;
        return cmpX == 0 ? o1.y - o2.y : cmpX;
    }
};
複製程式碼

定義了一個比較器,傳入了snake(下面分析)泛型,其實Diffutil只有這一個成員變數,然後就是內部類,下面先分析一下內部類:

Callback


public abstract static class Callback {

    public abstract int getOldListSize();
    public abstract int getNewListSize();
   //Item是否一致
    public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
   //Item的內容是否一致
    public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
   //看到Payload應該很容易之前研究RecyclerView的區域性重新整理中的payLoad,其實是一個意思
   //這個會傳到onBindHolder中去,作為區域性重新整理的標誌
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        return null;
    }
}
複製程式碼

Snake

這個直接翻譯過來是一條蛇,通過註釋來看,DiffUtil是採用了Myers差分演算法,所以Snake實際上代表的是蛇形路徑,就是將新舊集合的差異進一步描述出來


static class Snake {
    int x;//Position in the old list
    int y;//Position in the new list
    int size;// Number of matches. Might be 0
   
    boolean removal;//true代表移除,false代表新增
     //true, 表示在尾部新增或者移除
     //false,表示在頭部進行新增或刪除
    boolean reverse;
}
複製程式碼

如果你稍微瞭解一下Myers差分演算法,就比較好理解,因為新集合是在舊集合的基礎上進行變化得到的,通過增加,刪除乃至移動變換,所以需要知道是在當前元素的前面或者後面進行操作,並且需要知道是什麼操作,增加或者刪除等

Range

static class Range {
    int oldListStart, oldListEnd;
    int newListStart, newListEnd;
    public Range() {
    }
    public Range(int oldListStart, int oldListEnd, int newListStart, int newListEnd) {
        this.oldListStart = oldListStart;
        this.oldListEnd = oldListEnd;
        this.newListStart = newListStart;
        this.newListEnd = newListEnd;
    }
}
複製程式碼

貌似不需要註釋,很好理解,主要是記錄Snake的全域性座標

DiffResult

這個是DiffResult的核心方法

public static DiffResult calculateDiff(Callback cb, boolean detectMoves) {
    final int oldSize = cb.getOldListSize();
    final int newSize = cb.getNewListSize();
    //Snake集合
    final List<Snake> snakes = new ArrayList<>();
    //Range集合
    final List<Range> stack = new ArrayList<>();
    stack.add(new Range(0, oldSize, 0, newSize));
    final int max = oldSize + newSize + Math.abs(oldSize - newSize);
    final int[] forward = new int[max * 2];
    final int[] backward = new int[max * 2];
    // We pool the ranges to avoid allocations for each recursive call.
    final List<Range> rangePool = new ArrayList<>();
    while (!stack.isEmpty()) {
        final Range range = stack.remove(stack.size() - 1);
        //採用Myers差分演算法計算差分結果
        final Snake snake = diffPartial(cb, range.oldListStart, range.oldListEnd,
                range.newListStart, range.newListEnd, forward, backward, max);
       //省略了一些演算法,感興趣的可以自己去看原始碼
    }
    // 對差分結果進行排序
    Collections.sort(snakes, SNAKE_COMPARATOR);
    return new DiffResult(cb, snakes, forward, backward, detectMoves);
}
複製程式碼

dispatchUpdatesTo

呼叫此方法,可以進行重新整理操作

public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
  //繼續跟蹤dispatchUpdatesTo方法
    dispatchUpdatesTo(new ListUpdateCallback() {
        @Override
        public void onInserted(int position, int count) {
            adapter.notifyItemRangeInserted(position, count);
        }

        @Override
        public void onRemoved(int position, int count) {
            adapter.notifyItemRangeRemoved(position, count);
        }

        @Override
        public void onMoved(int fromPosition, int toPosition) {
            adapter.notifyItemMoved(fromPosition, toPosition);
        }

        @Override
        public void onChanged(int position, int count, Object payload) {
            adapter.notifyItemRangeChanged(position, count, payload);
        }
    });
}
複製程式碼

看完你會發現,就是一個回撥介面,來回撥adapter而已

dispatchUpdatesTo

根據計算得到的路徑,來對舊集合定向重新整理

public void dispatchUpdatesTo(ListUpdateCallback updateCallback) {
    final BatchingListUpdateCallback batchingCallback;
    if (updateCallback instanceof BatchingListUpdateCallback) {
        batchingCallback = (BatchingListUpdateCallback) updateCallback;
    } else {
        batchingCallback = new BatchingListUpdateCallback(updateCallback);
        updateCallback = batchingCallback;
    }
    final List<PostponedUpdate> postponedUpdates = new ArrayList<>();
    int posOld = mOldListSize;
    int posNew = mNewListSize;
    //遍歷通過Myers差分演算法生成最短路徑,由於是集合所以要從頭開始計算的話需要倒序遍歷
    for (int snakeIndex = mSnakes.size() - 1; snakeIndex >= 0; snakeIndex--) {
        final Snake snake = mSnakes.get(snakeIndex);
        final int snakeSize = snake.size;
        final int endX = snake.x + snakeSize;
        final int endY = snake.y + snakeSize;
        if (endX < posOld) {
           //右移,說明是刪除操作,進行相應處理
            dispatchRemovals(postponedUpdates, batchingCallback, endX, posOld - endX, endX);
        }

        if (endY < posNew) {
           //下移,說明是新增操作,進行相應處理
            dispatchAdditions(postponedUpdates, batchingCallback, endX, posNew - endY,
                    endY);
        }
      //如果複寫了DiffUtil.Callback的getChangePayload方法,將會支援RecyclerView的區域性重新整理
        for (int i = snakeSize - 1; i >= 0; i--) {
            if ((mOldItemStatuses[snake.x + i] & FLAG_MASK) == FLAG_CHANGED) {
       //分發DiffUtil.Callback的getChangePayload方法,最終會抵達adater
                batchingCallback.onChanged(snake.x + i, 1,
                        mCallback.getChangePayload(snake.x + i, snake.y + i));
            }
        }
        posOld = snake.x;
        posNew = snake.y;
    }
  	//回撥Adapter,跟一下原始碼
    batchingCallback.dispatchLastEvent();
}
複製程式碼

dispatchLastEvent

public void dispatchLastEvent() {
    if (mLastEventType == TYPE_NONE) {
        return;
    }
    switch (mLastEventType) {
        case TYPE_ADD:
            mWrapped.onInserted(mLastEventPosition, mLastEventCount);
            break;
        case TYPE_REMOVE:
            mWrapped.onRemoved(mLastEventPosition, mLastEventCount);
            break;
        case TYPE_CHANGE:
            mWrapped.onChanged(mLastEventPosition, mLastEventCount, mLastEventPayload);
            break;
    }
    mLastEventPayload = null;
    mLastEventType = TYPE_NONE;
}
複製程式碼

mWrapped實際上就是DiffUtil.Callback的例項,然後我們呼叫的時候傳入自定義的Callback介面,就好

用法簡介

上面說了很多原理,下面簡單介紹一下用法

  • 自定義DiffCallback繼承自DiffUtil.Callback,複寫相關方法
  • 呼叫DiffUtil.calculateDiff(new DiffCallBack(oldData, newData), true),生成diffResult;
  • 呼叫diffResult.dispatchUpdatesTo(mAdapter);

如果還有不熟悉的話可以直接在網上搜相關部落格,用法還是比較簡單的,不過有一點需要注意的是就是,在DiffUtil的註釋當中,說明Myers差分演算法本身是不支援item移動的,然後谷歌自己實現了一套演算法,支援item的移動,但是當資料量比較大的時候比較耗時,需要在子執行緒中進行計算,我個人覺得只要當前後兩個資料集合相似度較高的時候DiffUtil的效果會比較明顯,這種情況下的定向重新整理比較有意義。

總結

上面分析了一下ListView跟RecyclerView的快取原理,下面簡單對比分析一下

AbsListView RecyclerView
快取 View ViewHolder
定向重新整理 不支援 支援
區域性重新整理 不支援 支援
重新整理動畫 不支援 支援
Item點選 支援 不支援
分隔線 樣式單一 自定義樣式
佈局方式 列表/網格 自定義樣式
頭尾新增 支援 不支援

通過分析可能大家已經知道了RecyclerView相對於ListView的區別,如果我們需要頻繁的重新整理列表資料以及新增動畫的話,我們還是採用RecyclerView,對於前後資料量變化不大的新舊集合,還可以通過DiffUtil來進行差分,這樣就能夠實現定向重新整理以及區域性重新整理,否則建議使用ListView,因為ListView內建了很多Adapter,類似ArrayAdapter,SimpleAdapter,CursorAdapter。

還有一種情況就是宮格列切換的功能,如下圖

switch

那個時候RecyclerView剛出來,最開始是用ListView跟GridView實現的,就是搞了兩個,一個顯示,另外一個隱藏,由於當時是採用的SwipeRefreshLayout實現的,所以自定義的上拉載入,在ListView跟GridView之間切換的時候特別痛苦,後來採用了RecyclerView,真的是不要太簡單,動態設定一下LayoutManager就行,底部的載入進度條也可以通過設定GridLayoutManager.SpanSizeLookup可以很好的進行處理。

相關文章