1. 程式人生 > >Android 踩坑記錄(一)- Recyclerview的快取機制

Android 踩坑記錄(一)- Recyclerview的快取機制

起因

上週因為業務需要,要完成一個展示優惠券資訊的列表,列表內每張券都有詳細資訊,點選詳細資訊或者右面向下的箭頭,可以展開相應優惠券的詳細資訊。展開的同時新增兩個動畫,展開的佈局需要做緩慢展開的動畫,向下展開的箭頭需要做順時針180度旋轉變成向上收縮的狀態。
當時看到這覺得沒問題,一個RecyclerView就搞定了,在Adapter內對Item佈局內的View做一個屬性動畫,簡單省事。於是就開始愉快的敲著鍵盤寫了起來,等寫好一測試,Perfect!

正常效果圖

展開收起展開毫無問題,重新整理一下,(⊙o⊙)…問題來了,怎麼箭頭是向上的,我記得在onBindViewHolder裡已經設定Item中箭頭的狀態是向下的。趕緊Debug一下,的確是設定了向下的圖片。後來又分別展開了幾個Item,重新整理了一次列表,發現每次箭頭方向錯亂的位置還不固定。立馬反應過來,估計是條目複用出的問題。立馬開始查RecyclerView的Item快取機制。

RecyclerView條目快取機制

看了原始碼才發現,RecyclerView快取基本上是通過三個內部類管理的,Recycler、RecycledViewPool和ViewCacheExtension。

** Recycler:**

Recycler用於管理已經廢棄或者與RecyclerView分離的ViewHolder,為了方便理解這個類,整理了下面的資料,請結合Recycler的程式碼分析:

內部類的成員變數和他們的含義:

變數作用
mChangedScrap與RecyclerView分離的ViewHolder列表
mAttachedScrap未與RecyclerView分離的ViewHolder列表
mCachedViewsViewHolder快取列表
mViewCacheExtension開發者可以控制的ViewHolder快取的幫助類
mRecyclerPoolViewHolder快取池

程式碼裡面有個關鍵的方法,註釋來自引文:

ViewHolder getScrapViewForPosition(int position, int type, boolean dryRun) {
    final int scrapCount = mAttachedScrap.size();
    // 在還未detach的廢棄檢視中查找出來一個型別匹配(無效型別)的view.
    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())) { if (type != INVALID_TYPE && holder.getItemViewType() != type) { break; } // 表明這個ViewHolder是從廢棄的View集合中取出來的,可用於itemView的返回值。 holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); return holder; } } if (!dryRun) { // 找到已經隱藏,但是未被刪除的view,然後將其detach掉,detach scrap中。 View view = mChildHelper.findHiddenNonRemovedView(position, type); 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; } } // 在第一級檢視快取中查詢. final int cacheSize = mCachedViews.size(); for (int i = 0; i < cacheSize; i++) { final ViewHolder holder = mCachedViews.get(i); // invalid view holders may be in cache if adapter has stable ids as they can be // retrieved via getScrapViewForId if (!holder.isInvalid() && holder.getLayoutPosition() == position) { if (!dryRun) { mCachedViews.remove(i); } if (DEBUG) { Log.d(TAG, "getScrapViewForPosition(" + position + ", " + type + ") found match in cache: " + holder); } return holder; } } return null; }

RecycledViewPool:

RecycledViewPool類是用來快取Item用,是一個ViewHolder的快取池,如果多個RecyclerView之間用setRecycledViewPool(RecycledViewPool)設定同一個RecycledViewPool,他們就可以共享Item。其實RecycledViewPool的內部維護了一個Map,裡面以不同的viewType為Key儲存了各自對應的ViewHolder集合。可以通過提供的方法來修改內部快取的Viewholder。

下面來看下這個類的程式碼:

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

        private static final int DEFAULT_MAX_SCRAP = 5;

        public void clear() {
            mScrap.clear();
        }

        public void setMaxRecycledViews(int viewType, int max) {
            mMaxScrap.put(viewType, max);
            final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
            if (scrapHeap != null) {
                while (scrapHeap.size() > max) {
                    scrapHeap.remove(scrapHeap.size() - 1);
                }
            }
        }

        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;
        }

        int size() {
            int count = 0;
            for (int i = 0; i < mScrap.size(); i ++) {
                ArrayList<ViewHolder> viewHolders = mScrap.valueAt(i);
                if (viewHolders != null) {
                    count += viewHolders.size();
                }
            }
            return count;
        }

        public void putRecycledView(ViewHolder scrap) {
            final int viewType = scrap.getItemViewType();
            final ArrayList scrapHeap = getScrapHeapForType(viewType);
            if (mMaxScrap.get(viewType) <= scrapHeap.size()) {
                return;
            }
            if (DEBUG && scrapHeap.contains(scrap)) {
                throw new IllegalArgumentException("this scrap item already exists");
            }
            scrap.resetInternal();
            scrapHeap.add(scrap);
        }

        void attach(Adapter adapter) {
            mAttachCount++;
        }

        void detach() {
            mAttachCount--;
        }


        /**
         * Detaches the old adapter and attaches the new one.
         * <p>
         * RecycledViewPool will clear its cache if it has only one adapter attached and the new
         * adapter uses a different ViewHolder than the oldAdapter.
         *
         * @param oldAdapter The previous adapter instance. Will be detached.
         * @param newAdapter The new adapter instance. Will be attached.
         * @param compatibleWithPrevious True if both oldAdapter and newAdapter are using the same
         *                               ViewHolder and view types.
         */
        void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
boolean compatibleWithPrevious) {
            if (oldAdapter != null) {
                detach();
            }
            if (!compatibleWithPrevious && mAttachCount == 0) {
                clear();
            }
            if (newAdapter != null) {
                attach(newAdapter);
            }
        }

        private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {
            ArrayList<ViewHolder> scrap = mScrap.get(viewType);
            if (scrap == null) {
                scrap = new ArrayList<ViewHolder>();
                mScrap.put(viewType, scrap);
                if (mMaxScrap.indexOfKey(viewType) < 0) {
                    mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);
                }
            }
            return scrap;
        }
    }

這個類提供了四個公共方法:

返回值方法作用
voidclear()清空快取池
RecyclerView.ViewHoldergetRecycledView(int viewType)得到一個viewType型別的Item
voidputRecycledView(RecyclerView.ViewHolder scrap)把viewType型別的Item放入快取池
voidsetMaxRecycledViews(int viewType, int max)設定對應viewType型別的Item的最大快取數量

ViewCacheExtension:

我們先來看下程式碼:

    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.
         *
         * @param recycler The Recycler that can be used to bind the View
         * @param position The adapter position
         * @param type     The type of the View, defined by adapter
         * @return A View that is bound to the given position or NULL if there is no View to re-use
         * @see LayoutManager#ignoreView(View)
         */
        abstract public View getViewForPositionAndType(Recycler recycler, int position, int type);
    }

ViewCacheExtension的程式碼一看什麼都沒有,沒錯這是一個需要開發者重寫的類。上面Recycler裡呼叫Recycler.getViewForPosition(int)方法獲取View時,Recycler先檢查自己內部的attached scrap和一級快取,再檢查ViewCacheExtension.getViewForPositionAndType(Recycler, int, int),最後檢查RecyclerViewPool,從上面三個任何一個只要拿到View就不會呼叫下一個方法。所以我們可以重寫getViewForPositionAndType(Recycler recycler, int position, int type),在方法裡通過Recycler類控制View快取。注意:如果你重寫了這個類,Recycler不會在這個類中做快取View的操作,是否快取View完全由開發者控制。

總結

經過上面的分析,發現被屬性動畫修改過的ImageView在holder裡,被RecyclerView快取了之後,在別的Item又拿出來複用,雖然你設定了向下的背景圖片,但是這個ImageView是做過180旋轉的,所以設定一個向下的箭頭圖片還是向上的樣子。
看來以後像旋轉一類的簡單的動畫還是用View動畫就可以了,複雜的動畫再用屬性動畫。也可以重寫Adapter裡的void onViewDetachedFromWindow(VH holder)方法,在裡面拿到holder找到修改過的ImageView,恢復他原來的屬性,特別是有View被快取複用的時候一定記得恢復原來的屬性,否則就會出現這種混亂的情況。

引用