RecyclerView緩存機制(咋複用?)

這是RecyclerView緩存機制系列文章的第一篇,系列文章的目錄以下:java

  1. RecyclerView緩存機制(咋複用?)
  2. RecyclerView緩存機制(回收些啥?)
  3. RecyclerView緩存機制(回收去哪?)
  4. RecyclerView緩存機制(scrap view)

若是想直接看結論能夠移步到第四篇末尾(你會後悔的,過程更加精彩)。緩存

引子

  • 若是列表中每一個移出屏幕的表項都直接銷燬,移入時從新建立,很不經濟。因此RecyclerView引入了緩存機制。
  • 回收是爲了複用,複用的好處是有可能免去兩個昂貴的操做:
    1. 爲表項視圖綁定數據
    2. 建立表項視圖
  • 下面幾個問題對於理解「回收複用機制」很關鍵:
    1. what:回收什麼?複用什麼?
    2. where:回收到哪裏去?從哪裏得到複用?
    3. when:何時回收?何時複用?

這一篇試着從已知的知識出發在源碼中尋覓未知的「RecyclerView複用機制」。bash

(ps: 下文中的 粗斜體字 表示引導源碼閱讀的心裏戲)app

尋覓

觸發複用的衆多時機中必然包含下面這種:「當移出屏幕的表項從新回到界面」。表項本質上是一個View,屏幕上的表項必然須要依附於一棵View樹,即必然有一個父容器調用了addView()。而 RecyclerView繼承自 ViewGroup,遂以RecyclerView.addView()爲切入點向上搜尋複用的代碼。less

RecyclerView.java中全局搜索「addView」,發現RecyclerView()並無對addView()函數重載,但找到一處addView()的調用:ide

//RecyclerView是ViewGroup的子類
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
    ...
    private void initChildrenHelper() {
        mChildHelper = new ChildHelper(new ChildHelper.Callback() {
            ...
            @Override
            public void addView(View child, int index) {
                if (VERBOSE_TRACING) {
                    TraceCompat.beginSection("RV addView");
                }
                //直接調用ViewGroup.addView()
                RecyclerView.this.addView(child, index);
                if (VERBOSE_TRACING) {
                    TraceCompat.endSection();
                }
                dispatchChildAttached(child);
            }
        }
    }
    ...
}
複製代碼

ChildHelper.Callback.addView()爲起點沿着調用鏈繼續向上搜尋,經歷ChildHelper.addView()---LayoutManager.addViewInt()---LayoutManager.addView()最終到達LayoutManager.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");
            }
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            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);
            }
        }
        ...
}
複製代碼

addView(view)中傳入的view是函數layoutState.next()的返回值。猜想該函數是用來得到下一個表項的。表項不止一個,應該有一個循環不斷的得到下一個表項纔對。 沿着剛纔的調用鏈繼續往上搜尋,就會發現:的確有一個循環!佈局

public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
    ...
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        ...
        //recyclerview 剩餘空間
        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);
            ...
        }
        ...
    }
}
複製代碼

fill()是在onLayoutChildren()中被調用:post

/**
         * Lay out all relevant child views from the given adapter.
         * 佈局全部給定adapter中相關孩子視圖
         * 註釋太長了,省略了不相關信息
         * @param recycler         Recycler to use for fetching potentially cached views for a
         *                         position
         * @param state            Transient state of RecyclerView
         */
        public void onLayoutChildren(Recycler recycler, State state) {
            Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
        }
複製代碼

看完註釋,感受前面猜想應該是正確的。onLayoutChildren()是用來佈局RecyclerView中全部的表項的。回頭去看一下layoutState.next(),表項複用邏輯應該就在其中。性能

/**
     * Helper class that keeps temporary state while {LayoutManager} is filling out the empty
     * space.
     */
    static class LayoutState {       
       /**
         * Gets the view for the next element that we should layout.
         * 得到下一個元素的視圖用於佈局
         * Also updates current item index to the next item, based on {@link #mItemDirection}
         *
         * @return The next element that we should layout.
         */
        View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            //調用了Recycler.getViewForPosition()
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }
    }
複製代碼

最終調用了Recycler.getViewForPosition(),Recycler是回收器的意思,感受離想要找的「複用」邏輯愈來愈近了。 Recycler究竟是作什麼用的?

/**
     * A Recycler is responsible for managing scrapped or detached item views for reuse.
     *  Recycler負責管理scrapped和detached表項的複用
     * <p>A "scrapped" view is a view that is still attached to its parent RecyclerView but
     * that has been marked for removal or reuse.</p>
     */
    public final class Recycler {
      ...
    }
複製代碼

終於找到你~~ ,Recycler用於表項的複用!沿着Recycler.getViewForPosition()的調用鏈繼續向下搜尋,找到了一個關鍵函數(函數太長了,爲了防止頭暈,只列出了關鍵節點):

/**
         * Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
         * cache, the RecycledViewPool, or creating it directly.
         * 嘗試得到指定位置的ViewHolder,要麼從scrap,cache,RecycledViewPool中獲取,要麼直接從新建立
         * @return ViewHolder for requested position
         */
        @Nullable
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
            ...
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            //0 從changed scrap集合中獲取ViewHolder
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            //1. 經過position從attach scrap或一級回收緩存中獲取ViewHolder
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                ...
            }
            
            if (holder == null) {
                ...
                final int type = mAdapter.getItemViewType(offsetPosition);
                //2. 經過id在attach scrap集合和一級回收緩存中查找viewHolder
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    ...
                }
                //3. 從自定義緩存中獲取ViewHolder
                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);
                    ...
                }
                //4.從緩存池中拿ViewHolder
                if (holder == null) { // fallback to pool
                    ...
                    holder = getRecycledViewPool().getRecycledView(type);
                    ...
                }
                //全部緩存都沒有命中,只能建立ViewHolder
                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;
            }
            //只有invalid的viewHolder才能綁定視圖數據
            else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                if (DEBUG && holder.isRemoved()) {
                    throw new IllegalStateException("Removed holder should be bound and it should"
                            + " come here only in pre-layout. Holder: " + holder
                            + exceptionLabel());
                }
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                //得到ViewHolder後,綁定視圖數據
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }
            ...
            return holder;
        }
複製代碼
  • 函數的名字以「tryGet」開頭,「嘗試得到」表示可能得到失敗,再結合註釋中說的:「嘗試得到指定位置的ViewHolder,要麼從scrap,cache,RecycledViewPool中,要麼直接從新建立。」猜想scrap,cache,RecycledViewPool是回收表項的容器,至關於表項緩存,若是緩存未命中則只能從新建立。
  • 函數的返回值是ViewHolder難道回收和複用的是ViewHolder? 函數開頭聲明瞭局部變量ViewHolder holder = null;最終返回的也是這個局部變量,而且有4處holder == null的判斷,這樣的代碼結構是否是有點像緩存?每次判空意味着上一級緩存未命中並繼續嘗試新的獲取方法?緩存是否是有不止一種存儲形式? 讓咱們一次一次地看:

第一次嘗試

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
      if (mState.isPreLayout()) {
          holder = getChangedScrapViewForPosition(position);
          fromScrapOrHiddenOrCache = holder != null;
      }
      ...
}
複製代碼

只有在mState.isPreLayout()true時纔會作此次嘗試,這應該是一種特殊狀況,先忽略。

第二次嘗試

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
      if (holder == null) {
          holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
          //下面一段代碼蘊含着一個線索,買個伏筆,先把他略去
          ...
      }
      ...
}
複製代碼
  • 當第一次嘗試失敗後,嘗試經過getScrapOrHiddenOrCachedHolderForPosition()得到ViewHolder
  • 這裏故意省略了一段代碼,先埋個伏筆,待會分析。先沿着獲取ViewHolder的調用鏈繼續往下:
//省略非關鍵代碼
        /**
         * Returns a view for the position either from attach scrap, hidden children, or cache.
         * 從attach scrap,hidden children或者cache中得到指定位置上的一個ViewHolder
         */
        ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            final int scrapCount = mAttachedScrap.size();
            // Try first for an exact, non-invalid match from scrap.
            //1.在attached scrap中搜索ViewHolder
            for (int i = 0; i < scrapCount; i++) {
                final ViewHolder holder = mAttachedScrap.get(i);
                if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                        && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                    return holder;
                }
            }
            //2.從移除屏幕的視圖中搜索ViewHolder,找到了以後將他存入scrap回收集合中
            if (!dryRun) {
                View view = mChildHelper.findHiddenNonRemovedView(position);
                if (view != null) {
                    final ViewHolder vh = getChildViewHolderInt(view);
                    mChildHelper.unhide(view);
                    int layoutIndex = mChildHelper.indexOfChild(view);
                    ...
                    mChildHelper.detachViewFromParent(layoutIndex);
                    scrapView(view);
                    vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                            | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                    return vh;
                }
            }
            // Search in our first-level recycled view cache.
            //3.在緩存中搜索ViewHolder
            final int cacheSize = mCachedViews.size();
            for (int i = 0; i < cacheSize; i++) {
                final ViewHolder holder = mCachedViews.get(i);
                //若找到ViewHolder,還須要對ViewHolder的索引進行匹配判斷
                if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
                    ...
                    return holder;
                }
            }
            return null;
        }  
複製代碼

依次從三個地方搜索ViewHolder:1. mAttachedScrap 2. 隱藏表項 3. mCachedViews,找到當即返回。 其中mAttachedScrapmCachedViews做爲Recycler的成員變量,用來存儲一組ViewHolder

public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ...
        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
        ...
        RecycledViewPool mRecyclerPool;
    }
複製代碼
  • 看到這裏應該能夠初步得出結論:RecyclerView回收機制中,回收複用的對象是ViewHolder,且以ArrayList爲結構存儲在Recycler對象中
  • RecycledViewPool mRecyclerPool;看着也像是回收容器,那待會是否是也會到這裏拿 ViewHolder?
  • 值得注意的是,當成功從mCachedViews中獲取ViewHolder對象後,還須要對其索引進行判斷,這就意味着 mCachedViews中緩存的ViewHolder只能複用於指定位置 ,打個比方:手指向上滑動,列表向下滾動,第2個表項移出屏幕,第4個表項移入屏幕,此時再滑回去,第2個表項再次出現,這個過程當中第4個表項不能複用被回收的第2個表項的ViewHolder,由於他們的位置不一樣,而再次進入屏幕的第2個表項就能夠成功複用。 待會能夠對比一下其餘複用是否也須要索引判斷
  • 回到剛纔埋下的伏筆,把第二次嘗試獲取ViewHolder的代碼補全:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
      if (holder == null) {
          holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
          //下面一段代碼蘊含這一個線索,買個伏筆,先把他略去
          if (holder != null) {
               //檢驗ViewHolder有效性
               if (!validateViewHolderForOffsetPosition(holder)) {
                    // recycle holder (and unscrap if relevant) since it can not be used
                    if (!dryRun) {
                         // we would like to recycle this but need to make sure it is not used by
                         // animation logic etc.
                         holder.addFlags(ViewHolder.FLAG_INVALID);
                         if (holder.isScrap()) {
                              removeDetachedView(holder.itemView, false);
                              holder.unScrap();
                          } else if (holder.wasReturnedFromScrap()) {
                              holder.clearReturnedFromScrapFlag();
                          }
                          //若不知足有效性檢驗,則回收ViewHolder
                          recycleViewHolderInternal(holder);
                    }
                    holder = null;
               } else {
                    fromScrapOrHiddenOrCache = true;
               }
          }
      }
      ...
}
複製代碼

若是成功得到ViewHolder則檢驗其有效性,若檢驗失敗則將其回收。好不容易獲取了ViewHoler對象,一言不合就把他回收?難道對全部複用的 ViewHolder 都有這麼嚴格的檢驗嗎? 暫時沒法回答這些疑問,仍是先把複用邏輯看完吧:

第三次嘗試

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
      //只有當Adapter設置了id,纔會進行此次查找
      if (mAdapter.hasStableIds()) {
           holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);
           if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
           }
      }
      ...
}
複製代碼

這一次嘗試調用的函數名(「byId」)和上一次(「byPosition」)只是後綴不同。上一次是經過表項位置,這一次是經過表項id。內部實現也幾乎同樣,判斷的依據從表項位置變成表項id。爲表項設置id屬於特殊狀況,先忽略。

第四次嘗試

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
      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 (view != null) {
                //得到view對應的ViewHolder
                holder = getChildViewHolder(view);
                if (holder == null) {
                    throw new IllegalArgumentException("getViewForPositionAndType returned"
                                    + " a view which does not have a ViewHolder"
                                    + exceptionLabel());
                } else if (holder.shouldIgnore()) {
                   throw new IllegalArgumentException("getViewForPositionAndType returned"
                                    + " a view that is ignored. You must call stopIgnoring before"
                                    + " returning this view." + exceptionLabel());
                }
            }
      }
      ...
}
複製代碼

通過從mAttachedScrapmCachedViews獲取ViewHolder未果後,繼續嘗試經過ViewCacheExtension獲取:

/**
     * ViewCacheExtension is a helper class to provide an additional layer of view caching that can
     * be controlled by the developer.
     * ViewCacheExtension提供了額外的表項緩存層,用戶幫助開發者本身控制表項緩存
     * <p>
     * When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and
     * first level cache to find a matching View. If it cannot find a suitable View, Recycler will
     * call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking
     * {@link RecycledViewPool}.
     * 當Recycler從attached scrap和first level cache中未能找到匹配的表項時,它會在去RecycledViewPool中查找以前,先嚐試從自定義緩存中查找
     * <p>
     */
    public abstract static class ViewCacheExtension {

        /**
         * Returns a View that can be binded to the given Adapter position.
         * <p>
         * This method should <b>not</b> create a new View. Instead, it is expected to return
         * an already created View that can be re-used for the given type and position.
         * If the View is marked as ignored, it should first call
         * {@link LayoutManager#stopIgnoringView(View)} before returning the View.
         * <p>
         * RecyclerView will re-bind the returned View to the position if necessary.
         */
        public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
    }
複製代碼

註釋揭露了不少信息:ViewCacheExtension用於開發者自定義表項緩存,且這層緩存的訪問順序位於mAttachedScrapmCachedViews以後,RecycledViewPool以前。這和Recycler. tryGetViewHolderForPositionByDeadline()中的代碼邏輯一致,那接下來的第五次嘗試,應該是從 RecycledViewPool 中獲取 ViewHolder

第五次嘗試

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
      if (holder == null) { 
          ...
          //從回收池中獲取ViewHolder對象
          holder = getRecycledViewPool().getRecycledView(type);
          if (holder != null) {
               holder.resetInternal();
               if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
               }
          }
      }
      ...
}
複製代碼

前四次嘗試都未果,最後從RecycledViewPool中獲取ViewHolder稍等片刻!相對於從mAttachedScrapmCachedViews 中獲取 ViewHolder,此處並無嚴格的檢驗邏輯。爲啥要區別對待不一樣的緩存? 大大的問號懸在頭頂,但如今暫時沒法解答,仍是接着看RecycledViewPool的結構吧~

public final class Recycler {
    ...
    RecycledViewPool mRecyclerPool;
    //得到RecycledViewPool實例
    RecycledViewPool getRecycledViewPool() {
          if (mRecyclerPool == null) {
              mRecyclerPool = new RecycledViewPool();
          }
          return mRecyclerPool;
    }
    ...
}
public static class RecycledViewPool {
    ...
    //從回收池中獲取ViewHolder對象
    public ViewHolder getRecycledView(int viewType) {
          final ScrapData scrapData = mScrap.get(viewType);
          if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
              final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
              return scrapHeap.remove(scrapHeap.size() - 1);
          }
          return null;
    }
    ...
}
複製代碼

函數中只要訪問了類成員變量,它的複雜度就提升了,由於類成員變量的做用於超出了函數體,使得函數就和類中其餘函數耦合,因此不得不進行閱讀更多以幫助理解該函數:

public static class RecycledViewPool {
        //同類ViewHolder緩存個數上限
        private static final int DEFAULT_MAX_SCRAP = 5;

        /**
         * Tracks both pooled holders, as well as create/bind timing metadata for the given type.
         * 回收池中存放單個類型ViewHolder的容器
         */
        static class ScrapData {
            //同類ViewHolder存儲在ArrayList中
            ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            //每種類型的ViewHolder最多存5個
            int mMaxScrap = DEFAULT_MAX_SCRAP;
        }
        //回收池中存放全部類型ViewHolder的容器
        SparseArray<ScrapData> mScrap = new SparseArray<>();
        ...
        //ViewHolder入池 按viewType分類入池,一個類型的ViewType存放在一個ScrapData中
        public void putRecycledView(ViewHolder scrap) {
            final int viewType = scrap.getItemViewType();
            final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
            //若是超限了,則放棄入池
            if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
                return;
            }
            if (DEBUG && scrapHeap.contains(scrap)) {
                throw new IllegalArgumentException("this scrap item already exists");
            }
            scrap.resetInternal();
            //回收時,ViewHolder從列表尾部插入
            scrapHeap.add(scrap);
        }
        //從回收池中獲取ViewHolder對象
        public ViewHolder getRecycledView(int viewType) {
              final ScrapData scrapData = mScrap.get(viewType);
              if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
                  final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
                  //複用時,從列表尾部獲取ViewHolder(優先複用剛入池的ViewHoler)
                  return scrapHeap.remove(scrapHeap.size() - 1);
              }
              return null;
        }
}
複製代碼
  • 上述代碼列出了RecycledViewPool中最關鍵的一個成員變量和兩個函數。至此能夠得出結論:RecycledViewPool中的ViewHolder存儲在SparseArray中,而且按viewType分類存儲(便是Adapter.getItemViewType()的返回值),同一類型的ViewHolder存放在ArrayList中,且默認最多存儲5個。
  • 相比較於mCachedViews,從mRecyclerPool中成功獲取ViewHolder對象後並無作合法性和表項位置校驗,只檢驗viewType是否一致。因此 mRecyclerPool中取出的ViewHolder只能複用於相同viewType的表項

建立ViewHolder並綁定數據

ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
            ...
            //全部緩存都沒有命中,只能建立ViewHolder
            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()) {
                if (DEBUG && holder.isRemoved()) {
                    throw new IllegalStateException("Removed holder should be bound and it should"
                            + " come here only in pre-layout. Holder: " + holder
                            + exceptionLabel());
                }
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                //爲表項綁定數據
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }
}
複製代碼
  • 再進行了上述全部嘗試後,若是依然沒有得到ViewHolder,只能從新建立並綁定數據。沿着調用鏈往下,就會找到熟悉的onCreateViewHolder()onBindViewHolder()
  • 綁定數據的邏輯嵌套在一個大大的if中(原來並非每次都要綁定數據,只有知足特定條件時才須要綁定。
  • 那什麼狀況下須要綁定,什麼狀況下不須要呢?這就要引出「緩存優先級」這個概念。

緩存優先級

  • 緩存有優先級一說,在使用圖片二級緩存(內存+磁盤)時,會先嚐試去優先級高的內存中獲取,若未命中再去磁盤中獲取。優先級越高意味着性能越好。RecyclerView的緩存機制中是否也能套用「緩存優先級」這一邏輯?

  • 雖然爲了獲取ViewHolder作了5次嘗試(共從6個地方獲取),先排除3種特殊狀況,即從mChangedScrap獲取、經過id獲取、從自定義緩存獲取,正常流程中只剩下3種獲取方式,優先級從高到低依次是:

    1. mAttachedScrap獲取
    2. mCachedViews獲取
    3. mRecyclerPool獲取
  • 這樣的緩存優先級是否是意味着,對應的複用性能也是從高到低?(複用性能越好意味着所作的昂貴操做越少)

    1. 最壞狀況:從新建立ViewHodler並從新綁定數據
    2. 次好狀況:複用ViewHolder但從新綁定數據
    3. 最好狀況:複用ViewHolder且不從新綁定數據

    毫無疑問,全部緩存都未命中的狀況下會發生最壞狀況。剩下的兩種狀況應該由3種獲取方式來分攤,猜想優先級最低的 mRecyclerPool 方式應該命中次好狀況,而優先級最高的 mAttachedScrap應該命中最好狀況,去源碼中驗證一下:

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
       final int scrapCount = mAttachedScrap.size();

       // Try first for an exact, non-invalid match from scrap.
       //1.從attached scrap回收集合中
       for (int i = 0; i < scrapCount; i++) {
           final ViewHolder holder = mAttachedScrap.get(i);
           //只有當holder是有效時才返回
           if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                   && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
               holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
               return holder;
           }
       }
}

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
      if (holder == null) { 
          ...
          //從回收池中獲取ViewHolder對象
          holder = getRecycledViewPool().getRecycledView(type);
          if (holder != null) {
               //重置ViewHolder
               holder.resetInternal();
               if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
               }
          }
      }
      ...
      //若是表項沒有綁定過數據 或 表項須要更新 或 表項無效 且表項沒有被移除時綁定表項數據
      else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
          if (DEBUG && holder.isRemoved()) {
              throw new IllegalStateException("Removed holder should be bound and it should"
                            + " come here only in pre-layout. Holder: " + holder
                            + exceptionLabel());
          }
          final int offsetPosition = mAdapterHelper.findPositionOffset(position);
          //爲表項綁定數據
          bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
      }
      ...
}

public abstract static class ViewHolder {
        /**
         * This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType
         * are all valid.
         * 綁定標誌位
         */
        static final int FLAG_BOUND = 1 << 0;
        /**
         * This ViewHolder’s data is invalid. The identity implied by mPosition and mItemId
         * are not to be trusted and may no longer match the item view type.
         * This ViewHolder must be fully rebound to different data.
         * 無效標誌位
         */
        static final int FLAG_INVALID = 1 << 2;
        //判斷ViewHolder是否無效
        boolean isInvalid() {
            //將當前ViewHolder對象的flag和無效標誌位作位與操做
            return (mFlags & FLAG_INVALID) != 0;
        }
        //判斷ViewHolder是否被綁定
        boolean isBound() {
            //將當前ViewHolder對象的flag和綁定標誌位作位與操做
            return (mFlags & FLAG_BOUND) != 0;
        }
        /**
         * 將ViewHolder重置
         */
        void resetInternal() {
            //將ViewHolder的flag置0
            mFlags = 0;
            mPosition = NO_POSITION;
            mOldPosition = NO_POSITION;
            mItemId = NO_ID;
            mPreLayoutPosition = NO_POSITION;
            mIsRecyclableCount = 0;
            mShadowedHolder = null;
            mShadowingHolder = null;
            clearPayload();
            mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
            mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
            clearNestedRecyclerViewIfNotNested(this);
        }
}
複製代碼

溫故知新,回看 mRecyclerPool複用邏輯時,發如今成功得到ViewHolder對象後,當即對其重置(將flag置0)。這樣就知足了綁定數據的判斷條件(由於0和非0位與以後必然爲0)。 一樣的,在才mAttachedScrap中獲取ViewHolder時,只有當其是有效的纔會返回。因此猜想成立:mRecyclerPool中複用的ViewHolder須要從新綁定數據,從mAttachedScrap中複用的ViewHolder不要從新出建立也不須要從新綁定數據

總結

  1. RecyclerView中,並非每次繪製表項,都會從新建立ViewHolder對象,也不是每次都會從新綁定ViewHolder數據。
  2. RecyclerView經過Recycler得到下一個待繪製表項。
  3. Recycler有4個層次用於緩存ViewHolder對象,優先級從高到底依次爲ArrayList<ViewHolder> mAttachedScrapArrayList<ViewHolder> mCachedViewsViewCacheExtension mViewCacheExtensionRecycledViewPool mRecyclerPool。若是四層緩存都未命中,則從新建立並綁定ViewHolder對象
  4. RecycledViewPoolViewHolderviewType分類存儲(經過SparseArray),同類ViewHolder存儲在默認大小爲5的ArrayList
  5. mRecyclerPool中複用的ViewHolder須要從新綁定數據,從mAttachedScrap中複用的ViewHolder不須要從新建立也不須要從新綁定數據
  6. mRecyclerPool中複用的ViewHolder,只能複用於viewType相同的表項,從mCachedViews中複用的ViewHolder,只能複用於指定位置的表項。

這篇文章粗略的回答了關於「複用」的4個問題,即「複用什麼?」、「從哪裏得到複用?」、「何時複用?」、「複用優先級」。讀到這裏,可能會有不少疑問:

  1. scrap view是什麼?
  2. changed scrap viewattached scrap view有什麼區別?
  3. 複用的ViewHolder是在何時被緩存的?
  4. 爲何要4層緩存?它們的用途有什麼區別?

分析完「複用」,後續文章會進一步分析「回收」,但願到時候這些問題都能迎刃而解。

相關文章
相關標籤/搜索