Android 踩坑記錄(一)- Recyclerview的緩存機(jī)制

工作中遇到一些問(wèn)題胖笛,以此記錄問(wèn)題的解決過(guò)程网持。

起因

上周因?yàn)闃I(yè)務(wù)需要,要完成一個(gè)展示優(yōu)惠券信息的列表匀钧,列表內(nèi)每張券都有詳細(xì)信息翎碑,點(diǎn)擊詳細(xì)信息或者右面向下的箭頭,可以展開(kāi)相應(yīng)優(yōu)惠券的詳細(xì)信息之斯。展開(kāi)的同時(shí)添加兩個(gè)動(dòng)畫日杈,展開(kāi)的布局需要做緩慢展開(kāi)的動(dòng)畫,向下展開(kāi)的箭頭需要做順時(shí)針180度旋轉(zhuǎn)變成向上收縮的狀態(tài)佑刷。
當(dāng)時(shí)看到這覺(jué)得沒(méi)問(wèn)題莉擒,一個(gè)RecyclerView就搞定了,在Adapter內(nèi)對(duì)Item布局內(nèi)的View做一個(gè)屬性動(dòng)畫瘫絮,簡(jiǎn)單省事涨冀。于是就開(kāi)始愉快的敲著鍵盤寫了起來(lái),等寫好一測(cè)試麦萤,Perfect鹿鳖!

正常效果圖

展開(kāi)收起展開(kāi)毫無(wú)問(wèn)題,刷新一下壮莹,(⊙o⊙)…問(wèn)題來(lái)了翅帜,怎么箭頭是向上的,我記得在onBindViewHolder里已經(jīng)設(shè)置Item中箭頭的狀態(tài)是向下的命满。趕緊Debug一下涝滴,的確是設(shè)置了向下的圖片。后來(lái)又分別展開(kāi)了幾個(gè)Item,刷新了一次列表歼疮,發(fā)現(xiàn)每次箭頭方向錯(cuò)亂的位置還不固定杂抽。立馬反應(yīng)過(guò)來(lái),估計(jì)是條目復(fù)用出的問(wèn)題韩脏。立馬開(kāi)始查RecyclerView的Item緩存機(jī)制缩麸。

RecyclerView條目緩存機(jī)制

看了源碼才發(fā)現(xiàn),RecyclerView緩存基本上是通過(guò)三個(gè)內(nèi)部類管理的骤素,Recycler匙睹、RecycledViewPool和ViewCacheExtension。

** Recycler:**

Recycler用于管理已經(jīng)廢棄或者與RecyclerView分離的ViewHolder济竹,為了方便理解這個(gè)類,整理了下面的資料霎槐,請(qǐng)結(jié)合Recycler的代碼分析:

內(nèi)部類的成員變量和他們的含義:

變量 作用
mChangedScrap 與RecyclerView分離的ViewHolder列表
mAttachedScrap 未與RecyclerView分離的ViewHolder列表
mCachedViews ViewHolder緩存列表
mViewCacheExtension 開(kāi)發(fā)者可以控制的ViewHolder緩存的幫助類
mRecyclerPool ViewHolder緩存池

代碼里面有個(gè)關(guān)鍵的方法送浊,注釋來(lái)自引文:

ViewHolder getScrapViewForPosition(int position, int type, boolean dryRun) {
    final int scrapCount = mAttachedScrap.size();
    // 在還未detach的廢棄視圖中查找出來(lái)一個(gè)類型匹配(無(wú)效類型)的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;
            }
      // 表明這個(gè)ViewHolder是從廢棄的View集合中取出來(lái)的,可用于itemView的返回值丘跌。
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            return holder;
        }
    }
    if (!dryRun) {
    // 找到已經(jīng)隱藏袭景,但是未被刪除的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;
        }
    }
    // 在第一級(jí)視圖緩存中查找.
    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類是用來(lái)緩存Item用,是一個(gè)ViewHolder的緩存池报辱,如果多個(gè)RecyclerView之間用setRecycledViewPool(RecycledViewPool)設(shè)置同一個(gè)RecycledViewPool与殃,他們就可以共享Item。其實(shí)RecycledViewPool的內(nèi)部維護(hù)了一個(gè)Map碍现,里面以不同的viewType為Key存儲(chǔ)了各自對(duì)應(yīng)的ViewHolder集合幅疼。可以通過(guò)提供的方法來(lái)修改內(nèi)部緩存的Viewholder昼接。

下面來(lái)看下這個(gè)類的代碼:

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

這個(gè)類提供了四個(gè)公共方法:

返回值 方法 作用
void clear() 清空緩存池
RecyclerView.ViewHolder getRecycledView(int viewType) 得到一個(gè)viewType類型的Item
void putRecycledView(RecyclerView.ViewHolder scrap) 把viewType類型的Item放入緩存池
void setMaxRecycledViews(int viewType, int max) 設(shè)置對(duì)應(yīng)viewType類型的Item的最大緩存數(shù)量

ViewCacheExtension:

我們先來(lái)看下代碼:

    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的代碼一看什么都沒(méi)有爽篷,沒(méi)錯(cuò)這是一個(gè)需要開(kāi)發(fā)者重寫的類。上面Recycler里調(diào)用Recycler.getViewForPosition(int)方法獲取View時(shí)慢睡,Recycler先檢查自己內(nèi)部的attached scrap和一級(jí)緩存逐工,再檢查ViewCacheExtension.getViewForPositionAndType(Recycler, int, int),最后檢查RecyclerViewPool漂辐,從上面三個(gè)任何一個(gè)只要拿到View就不會(huì)調(diào)用下一個(gè)方法泪喊。所以我們可以重寫getViewForPositionAndType(Recycler recycler, int position, int type),在方法里通過(guò)Recycler類控制View緩存者吁。注意:如果你重寫了這個(gè)類窘俺,Recycler不會(huì)在這個(gè)類中做緩存View的操作,是否緩存View完全由開(kāi)發(fā)者控制。

總結(jié)

經(jīng)過(guò)上面的分析瘤泪,發(fā)現(xiàn)被屬性動(dòng)畫修改過(guò)的ImageView在holder里灶泵,被RecyclerView緩存了之后,在別的Item又拿出來(lái)復(fù)用对途,雖然你設(shè)置了向下的背景圖片赦邻,但是這個(gè)ImageView是做過(guò)180旋轉(zhuǎn)的,所以設(shè)置一個(gè)向下的箭頭圖片還是向上的樣子实檀。
看來(lái)以后像旋轉(zhuǎn)一類的簡(jiǎn)單的動(dòng)畫還是用View動(dòng)畫就可以了惶洲,復(fù)雜的動(dòng)畫再用屬性動(dòng)畫。也可以重寫Adapter里的void onViewDetachedFromWindow(VH holder)方法膳犹,在里面拿到holder找到修改過(guò)的ImageView恬吕,恢復(fù)他原來(lái)的屬性,特別是有View被緩存復(fù)用的時(shí)候一定記得恢復(fù)原來(lái)的屬性须床,否則就會(huì)出現(xiàn)這種混亂的情況铐料。

引用

部分內(nèi)容來(lái)自以下博客,特此鳴謝博客作者的分享:
RecyclerView解析
RecyclerView源碼分析

本文內(nèi)容采用 CC BY-NC-SA 3.0 進(jìn)行許可, 轉(zhuǎn)載請(qǐng)注明出處, 版權(quán)歸本人及所有貢獻(xiàn)者所有豺旬。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末钠惩,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子族阅,更是在濱河造成了極大的恐慌篓跛,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坦刀,死亡現(xiàn)場(chǎng)離奇詭異愧沟,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)求泰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門央渣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人渴频,你說(shuō)我怎么就攤上這事芽丹。” “怎么了卜朗?”我有些...
    開(kāi)封第一講書人閱讀 157,435評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵拔第,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我场钉,道長(zhǎng)蚊俺,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,509評(píng)論 1 284
  • 正文 為了忘掉前任逛万,我火速辦了婚禮泳猬,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己得封,他們只是感情好埋心,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,611評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著忙上,像睡著了一般拷呆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上疫粥,一...
    開(kāi)封第一講書人閱讀 49,837評(píng)論 1 290
  • 那天茬斧,我揣著相機(jī)與錄音,去河邊找鬼梗逮。 笑死项秉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的慷彤。 我是一名探鬼主播伙狐,決...
    沈念sama閱讀 38,987評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼瞬欧!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起罢防,我...
    開(kāi)封第一講書人閱讀 37,730評(píng)論 0 267
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤艘虎,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后咒吐,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體野建,經(jīng)...
    沈念sama閱讀 44,194評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,525評(píng)論 2 327
  • 正文 我和宋清朗相戀三年恬叹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了候生。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,664評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡绽昼,死狀恐怖唯鸭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情硅确,我是刑警寧澤目溉,帶...
    沈念sama閱讀 34,334評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站菱农,受9級(jí)特大地震影響缭付,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜循未,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,944評(píng)論 3 313
  • 文/蒙蒙 一陷猫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦绣檬、人聲如沸足陨。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,764評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)钠右。三九已至,卻和暖如春忘蟹,著一層夾襖步出監(jiān)牢的瞬間飒房,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,997評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工媚值, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留狠毯,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,389評(píng)論 2 360
  • 正文 我出身青樓褥芒,卻偏偏與公主長(zhǎng)得像嚼松,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子锰扶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,554評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容