RecyclerView緩存復(fù)用原理

RecyclerView緩存復(fù)用機(jī)制

來到RecyclerView的Adapter代碼中:

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.item, parent, false);
        Log.i("minfo", "onCreateViewHolder");
        return new MyViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
        ((TextView)holder.textView).setText(datas.get(position));
        Log.i("minfo", "onBindViewHolder");
    }

在onCreateViewHolder方法和onBindViewHolder方法打印关炼,不斷滑動(dòng)recyclerView盏浇,會(huì)發(fā)現(xiàn)一個(gè)現(xiàn)象就是剛開始都會(huì)調(diào)兩個(gè)方法,但是調(diào)用一定次數(shù)之后,就只會(huì)調(diào)onBindViewHolder方法了斩箫。那么這個(gè)滑動(dòng)過程中旨怠,就是在不斷地回收復(fù)用拷泽。

首先要明白夹姥,RecyclerView緩存復(fù)用的是什么?緩存復(fù)用的是ViewHolder婶溯,ViewHolder是itemView的封裝類鲸阔。

Recycler 是 RecyclerView 的內(nèi)部類,也是這套復(fù)用機(jī)制的核心迄委,顯然 Recycler 的主要成員變量也都是用來緩存和復(fù)用 ViewHolder 的:

public final class Recycler {
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    ArrayList<ViewHolder> mChangedScrap = null;
 
    final ArrayList<ViewHolder> mCachedViews = new ArrayList<>();
    
    RecycledViewPool mRecyclerPool;
    
    private ViewCacheExtension mViewCacheExtension;
}
復(fù)用機(jī)制

在滑動(dòng)過程中褐筛,recyclerView不斷在復(fù)用ViewHolder來顯示item,那么就從recyclerView的滑動(dòng)事件入手源碼叙身,然后步步深入:

 RecyclerView.onLayout(...)
-> RecyclerView.dispatchLayout()    
-> RecyclerView.dispatchLayoutStep2() // do the actual layout of the views for the final state.
-> mLayout.onLayoutChildren(mRecycler, mState) // mLayout 類型為 LayoutManager
-> LinearLayoutManager.onLayoutChildren(...) // 以 LinearLayoutManager 為例
-> LinearLayoutManager.fill(...) // The magic functions :) 填充給定的布局渔扎,注釋很自信的說這個(gè)方法很獨(dú)立,稍微改動(dòng)就能作為幫助類的一個(gè)公開方法信轿,程序員的快樂就是這么樸實(shí)無華晃痴。
-> LinearLayoutManager.layoutChunk(recycler, layoutState) // 循環(huán)調(diào)用,每次調(diào)用填充一個(gè) ItemView 到 RV
-> LinearLayoutManager.LayoutState.next(recycler) 
-> RecyclerView.Recycler.getViewForPosition(int) // 回到主角了,通過 Recycler 獲取指定位置的 ItemView    
-> Recycler.getViewForPosition(int, boolean) // 調(diào)用下面方法獲取 ViewHolder,并返回上面需要的 viewHolder.itemView 
-> Recycler.tryGetViewHolderForPositionByDeadline(...)

最終調(diào)用 tryGetViewHolderForPositionByDeadline方法來獲取ViewHolder财忽,這個(gè)方法里面是復(fù)用的核心倘核,方法部分代碼:

ViewHolder tryGetViewHolderForPositionByDeadline(int position, ...) {
    if (mState.isPreLayout()) {
        // 從 mChangedScrap 里面去獲取 ViewHolder,動(dòng)畫相關(guān)
        holder = getChangedScrapViewForPosition(position);
    }
    
    if (holder == null) {
        // mAttachedScrap即彪、 mHiddenViews紧唱、mCachedViews 獲取 ViewHolder
        //    這個(gè) mHiddenViews 是用來做動(dòng)畫期間的復(fù)用
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    }
    
    if (holder == null) {
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 如果 Adapter 的 hasStableIds 方法返回為 true
        //    優(yōu)先通過 ViewType 和 ItemId 兩個(gè)條件從 mAttachedScrap 和 mCachedViews 尋找
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
        }
      
        if (holder == null && mViewCacheExtension != null) {
            //從自定義緩存獲取
            View view = mViewCacheExtension
                getViewForPositionAndType(this, position, type);
            holder = getChildViewHolder(view);
        }
    }
    
    if (holder == null) {
        //從 RecycledViewPool 獲取 ViewHolder
        holder = getRecycledViewPool().getRecycledView(type);
    }
  
    if (holder == null) {
        // 如果四級(jí)緩存里都沒有,那就再調(diào)用createViewHolder創(chuàng)建ViewHolder
        holder = mAdapter.createViewHolder(RecyclerView.this, type);
    }
}

結(jié)合RecyclerView類中的源碼及注釋可知隶校,Recycler會(huì)依次從mChangedScrap/mAttachedScrap漏益、mCachedViews、mViewCacheExtension深胳、mRecyclerPool中嘗試獲取指定位置或ID的ViewHolder對(duì)象以供重用绰疤,如果全都獲取不到則直接重新創(chuàng)建。

緩存機(jī)制
總共有四級(jí)緩存舞终,按照優(yōu)先級(jí)分:
  • 一級(jí)緩存:

一級(jí)緩存的代碼:

        void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
                if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
                    throw new IllegalArgumentException("Called scrap view with an invalid view."
                            + " Invalid views cannot be reused from scrap, they should rebound from"
                            + " recycler pool." + exceptionLabel());
                }
                holder.setScrapContainer(this, false);
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this, true);
                mChangedScrap.add(holder);
            }
        }

scrap是用來保存被rv移除掉但最近又馬上要使用的緩存轻庆,比如說rv中自帶item的動(dòng)畫效果癣猾。

mChangedScrap/mAttachedScrap

稍微仔細(xì)看的話就能發(fā)現(xiàn)scrap緩存有兩個(gè)成員mChangedScrap和mAttachedScrap,它們保存的對(duì)象有些不一樣余爆,一般調(diào)用adapter的notifyItemRangeChanged被移除的viewholder會(huì)保存到mChangedScrap煎谍,其余的notify系列方法(不包括notifyDataSetChanged)移除的viewholder會(huì)被保存到mAttachedScrap中。用來緩存還在屏幕內(nèi)的viewholder龙屉。

二級(jí)緩存:mCachedViews ,用來緩存移除屏幕之外的 ViewHolder满俗,默認(rèn)情況下緩存容量是 2转捕,可以通過 setViewCacheSize 方法來改變緩存的容量大小。如果 mCachedViews 的容量已滿唆垃,則會(huì)根據(jù) 先進(jìn)先出 的規(guī)則移除舊 ViewHolder五芝,再將viewHolder添加進(jìn)RecycledViewPool中。

三級(jí)緩存:ViewCacheExtension 辕万,開發(fā)給用戶的自定義擴(kuò)展緩存枢步,需要用戶自己管理 View 的創(chuàng)建和緩存。通過提供給開發(fā)者抽象方法來自己實(shí)現(xiàn)緩存渐尿。

四級(jí)緩存:RecycledViewPool 醉途,ViewHolder 緩存池,在有限的 mCachedViews 中如果存不下新的 ViewHolder 時(shí)砖茸,會(huì)從cachedView緩存中移除隘擎,然后將移除的 ViewHolder 存入RecyclerViewPool 中。

將緩存viewHolder添加到recyclerViewPool中凉夯,根據(jù)itemtype獲取對(duì)應(yīng)的ScrapData數(shù)據(jù)货葬,里面存儲(chǔ)著該itemType的緩存集合。RecycledViewPool默認(rèn)大小為5劲够,當(dāng)大于了最大容量震桶,就不再緩存。

可以通過以下方式修改RecycledViewPool的緩存大姓饕铩:

RecyclerView.getRecycledViewPool().setMaxRecycledViews(int viewType, int max);
        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();
            scrapHeap.add(scrap);
        }
 
//根據(jù)itemtype獲取對(duì)應(yīng)的ScrapData數(shù)據(jù)
private ScrapData getScrapDataForType(int viewType) {
            ScrapData scrapData = mScrap.get(viewType);
            if (scrapData == null) {
                scrapData = new ScrapData();
                mScrap.put(viewType, scrapData);
            }
            return scrapData;
        }
mCachedViews二級(jí)緩存與四級(jí)緩存RecycledViewPool的區(qū)別:

cacheView緩存的viewHolder帶有數(shù)據(jù)蹲姐,而四級(jí)緩存已經(jīng)清空了數(shù)據(jù),只是緩存了viewHolder炒瘸,從 RecyclerViewPool 中取出來的 ViewHolder 需要重新執(zhí)行 bindViewHolder才能使用淤堵。所以二級(jí)緩存的復(fù)用速度會(huì)比四級(jí)緩存的復(fù)用速度更高。

mCachedViews只會(huì)緩存2個(gè)viewholder顷扩,就是屏幕滑出去的上面一個(gè)item和下面一個(gè)item拐邪。當(dāng)上面一個(gè)item剛滑出去或者下面一個(gè)item剛滑出去,緩存在mCachedViews里面隘截,內(nèi)容不會(huì)被清空扎阶。此時(shí)再把這個(gè)item滑入屏幕內(nèi)汹胃,不會(huì)走onBindViewHolder,因?yàn)閿?shù)據(jù)還在东臀。而隨著往上或往下繼續(xù)滑動(dòng)更多着饥,移除屏幕的item超過2個(gè),mCachedViews緩存容量不足惰赋,就會(huì)將滑出屏幕的viewholder往recyclerViewPool中緩存宰掉。而把這些更多的item滑回屏幕時(shí),這些緩存的viewholder數(shù)據(jù)已經(jīng)被清掉赁濒,所以會(huì)調(diào)用onBindViewHolder方法重設(shè)數(shù)據(jù)

繪圖總結(jié):

參考:
RecyclerView 的緩存復(fù)用機(jī)制
https://www.bilibili.com/video/BV1Yb4y1R7xn?p=3&spm_id_from=pageDriver&vd_source=40c24e77b23dc2e50de2b7c87c6fed59

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末轨奄,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子拒炎,更是在濱河造成了極大的恐慌挪拟,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件击你,死亡現(xiàn)場離奇詭異玉组,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)丁侄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門惯雳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人绒障,你說我怎么就攤上這事吨凑。” “怎么了户辱?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵鸵钝,是天一觀的道長。 經(jīng)常有香客問我庐镐,道長恩商,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任必逆,我火速辦了婚禮怠堪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘名眉。我一直安慰自己粟矿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布损拢。 她就那樣靜靜地躺著陌粹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪福压。 梳的紋絲不亂的頭發(fā)上掏秩,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天或舞,我揣著相機(jī)與錄音,去河邊找鬼蒙幻。 笑死映凳,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的邮破。 我是一名探鬼主播诈豌,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼抒和!你這毒婦竟也來了队询?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤构诚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后铆惑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體范嘱,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年员魏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了丑蛤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡撕阎,死狀恐怖受裹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情虏束,我是刑警寧澤棉饶,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站镇匀,受9級(jí)特大地震影響照藻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜汗侵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一幸缕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧晰韵,春花似錦发乔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至浪蹂,卻和暖如春抵栈,著一層夾襖步出監(jiān)牢的瞬間告材,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國打工古劲, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留斥赋,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓产艾,卻偏偏與公主長得像疤剑,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子闷堡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

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