RecyclerView#Adapter#notifyDataSetChanged方法后盅弛,為何還會新建ViewHolder?

環(huán)境

android sdk版本: 30

依賴:

implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.recyclerview:recyclerview:1.2.1"

案例分析:

RecyclerView寬高固定铲汪;LayoutManagerLienarLayoutManager熊尉,vertical方向;數(shù)據(jù)20條掌腰,足以鋪滿整個屏幕狰住。

現(xiàn)象:
①先創(chuàng)建Adapter,設置20條數(shù)據(jù)齿梁。
②調(diào)用RecyclerView#Adapter#notifyDataSetChanged方法后催植,當前頁面中只有5個ViewHolder復用,其余的ViewHolder會走Adapter#createViewHolder方法創(chuàng)建新的ViewHolder勺择。

原理:

為了搞清楚原理创南,我們先看一下,剛進入頁面時省核,RecyclerView#Adapter#onCreateViewHolder方法的調(diào)用棧稿辙。

RecyclerView#Adapter#onCreateViewHolder方法的調(diào)用棧

RecyclerView#onLayout(): 4578行
RecyclerView#dispatchLayout(): 4012行
RecyclerView#dispatchLayoutStep2():4309行
LinearLayoutManager#onLayoutChildren(): 668行
LinearLayoutManager#fill(): 1591行
LinearLayoutManager#layoutChunk(): 1631行
LinearLayoutManager#LayoutState#next(): 2330行
RecyclerView#Recycler#getViewForPosition(int position): 6296行
RecyclerView#Recycler#getViewForPosition(position, boolean dryRun): 6300行
RecyclerView#Recycler#tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs): 6416行
RecyclerView#Adapter#createViewHolder(@NonNull ViewGroup parent, int viewType): 7295行
RecyclerView#Adapter#onCreateViewHolder(@NonNull ViewGroup parent, int viewType)

其核心是RecyclerView#Recycler#tryGetViewHolderForPositionByDeadline方法,它主要有兩個作用气忠,一個是獲取ViewHolder邻储;另一個給ViewHolder綁定數(shù)據(jù)赋咽。

獲取ViewHolder是有順序的,會先嘗試從各級緩存里面去獲取吨娜,會依次從Recycler scrap脓匿、cacheRecycledViewPool中獲取宦赠,如果都獲取不到陪毡,就直接創(chuàng)建一個ViewHolder

RecyclerView#Recycler#tryGetViewHolderForPositionByDeadline

Attempts to get the ViewHolder for the given position, either from the Recycler scrap, cache, the RecycledViewPool, or creating it directly.
獲取給定位置的ViewHolder勾扭。會依次從Recycler scrap毡琉、cache、RecycledViewPool中獲取尺借,如果都獲取不到绊起,就直接創(chuàng)建一個ViewHolder

核心:獲取viewHolder;給viewHolder綁定數(shù)據(jù)燎斩。
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    ...
    ViewHolder holder = null;
    // 0) If there is a changed scrap, try to find from there
    // 如果需要預先布局虱歪,就嘗試從mChangedScrap中去獲取。
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        ...
    }
    // 1) Find by position from scrap/hidden list/cache
    // 嘗試依次從mAttachedScrap栅表、mChildHelper的mHiddenViews笋鄙、mCachedViews中去獲取可復用的ViewHolder
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        // 校驗holder是否有效,無效就清除vh
        if (holder != null) {
            if (!validateViewHolderForOffsetPosition(holder)) {
                ...
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        ...
        // 獲取這個位置對應的數(shù)據(jù)類型怪瓶,通過重寫的Adapter#getItemViewType方法萧落。
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) Find from scrap/cache via stable ids, if exists
        // 如果設置了stable ids,就根據(jù)id依次從mAttachedScrap洗贰、mCachedViews中查找
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
            ...
        }
        // 嘗試從mViewCacheExtension中獲取VH
        if (holder == null && mViewCacheExtension != null) {
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
                ...
            }
        }
        // 嘗試從 RecycledViewPool 中獲取
        if (holder == null) { // fallback to pool
            ...
            holder = getRecycledViewPool().getRecycledView(type);
            ...
        }
        // 調(diào)用RecyclerView#Adapter#onCreateViewHolder生成ViewHolder
        if (holder == null) {
            ...
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            ...           
        }
    }
    ...
    // 給viewholder綁定數(shù)據(jù)
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        ...
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {       
        ...
        // 給viewholder綁定數(shù)據(jù)
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    // 給holder.itemView設置RecyclerView#LayoutParams找岖。并將其相互綁定。
    ...
    return holder;
}

實例分析敛滋。

我們這里調(diào)用RecyclerView#Adapter#notifyDataSetChanged方法后许布,既有復用的ViewHolder,也有新建的ViewHolder绎晃。復用的ViewHolder來自于哪里蜜唾?為什么是5個?為什么還要新建ViewHolder庶艾?

帶著這些問題袁余,我們debug下我們的場景,看下ViewHolder的來源咱揍。

核心在于調(diào)用RecyclerView#Recycler#tryGetViewHolderForPositionByDeadline方法颖榜,關鍵在于下面這段代碼:

holder = getRecycledViewPool().getRecycledView(type);

我們知道,RecycledViewPool中是以viewType來存放不同的ViewHolder的,每個type最多存放五個朱转。

所以我們在RecyclerView#Recycler#tryGetViewHolderForPositionByDeadline中蟹地,從RecycledViewPool中最多能找到五個可復用的ViewHolder积暖,其余的只能走新建ViewHolder流程了藤为。

RecycledViewPool

先來看下RecycledViewPool的說明:

RecycledViewPool lets you share Views between multiple RecyclerViews.
If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool and use setRecycledViewPool(RecyclerView.RecycledViewPool).
RecyclerView automatically creates a pool for itself if you don't provide one.

大意是RecycledViewPool可以讓你在多個recyclerview之間共享視圖。
如果你想在RecyclerViews中回收視圖夺刑,可以創(chuàng)建一個RecycledViewPool的實例并使用setRecycledViewPool(RecyclerView.RecycledViewPool)缅疟。
如果你不提供一個RecycledViewPool實例,那么RecyclerView會自動為自己創(chuàng)建一個遍愿。

我們看下RecyclerView#Recycler#getRecycledViewPool方法:確實是自動創(chuàng)建了一個存淫。

RecycledViewPool getRecycledViewPool() {
    if (mRecyclerPool == null) {
        mRecyclerPool = new RecycledViewPool();
    }
    return mRecyclerPool;
}

再看下RecycledViewPool的結構:

public static class RecycledViewPool {
    private static final int DEFAULT_MAX_SCRAP = 5;

    static class ScrapData {
        final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }

    SparseArray<ScrapData> mScrap = new SparseArray<>();
    ...
}

如上所示,里面有一個SparseArray<ScrapData>類型的變量mScrap沼填,用來存儲不同類型的ViewHolder桅咆。ScrapData數(shù)據(jù)中包含ArrayList<ViewHolder>類型變量mScrapHeap,用來存放具體的ViewHolder坞笙,它的最大容量是5(DEFAULT_MAX_SCRAP)岩饼。

RecycledViewPool中的數(shù)據(jù)何時添加的

本例中RecycledViewPool中的數(shù)據(jù)是從哪里添加的呢?

本例中薛夜,向ScrapData#mScrapHeap添加ViewHolder數(shù)據(jù)的調(diào)用鏈如下:

RecyclerView#onLayout(): 4578行
RecyclerView#dispatchLayout(): 4012行
RecyclerView#dispatchLayoutStep2():4309行
LinearLayoutManager#onLayoutChildren(): 633行
RecyclerView#LayoutManager#detachAndScrapAttachedViews(): 9493行
RecyclerView#LayoutManager#scrapOrRecycleView(): 9508行
RecyclerView#Recycler#recycleViewHolderInternal(): 6671行
RecyclerView#Recycler#addViewHolderToRecycledViewPool(): 6723行
RecyclerView#RecyclerViewPool#putRecycledView(ViewHolder scrap): 5931行
scrapHeap.add(scrap);

核心是LinearLayoutManager#onLayoutChildren()方法中籍茧,如下的這段代碼:

detachAndScrapAttachedViews(recycler);

也就是說,在調(diào)用RecyclerView#Adapter#notifyDataSetChanged方法后梯澜,會觸發(fā)繪制流程寞冯。在Linearlayout#layoutChildren方法中,會先對ViewHolder進行緩存晚伙,然后會對ViewHolder進行復用吮龄。

相關資料

demo-AdapterOnCreateViewHolderTestActivity

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市咆疗,隨后出現(xiàn)的幾起案子漓帚,更是在濱河造成了極大的恐慌,老刑警劉巖民傻,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胰默,死亡現(xiàn)場離奇詭異,居然都是意外死亡漓踢,警方通過查閱死者的電腦和手機牵署,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來喧半,“玉大人奴迅,你說我怎么就攤上這事。” “怎么了取具?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵脖隶,是天一觀的道長。 經(jīng)常有香客問我暇检,道長产阱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任块仆,我火速辦了婚禮构蹬,結果婚禮上,老公的妹妹穿的比我還像新娘悔据。我一直安慰自己庄敛,他們只是感情好,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布科汗。 她就那樣靜靜地躺著藻烤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪头滔。 梳的紋絲不亂的頭發(fā)上怖亭,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機與錄音拙毫,去河邊找鬼依许。 笑死,一個胖子當著我的面吹牛缀蹄,可吹牛的內(nèi)容都是我干的峭跳。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼缺前,長吁一口氣:“原來是場噩夢啊……” “哼蛀醉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起衅码,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤拯刁,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后逝段,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體垛玻,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年奶躯,在試婚紗的時候發(fā)現(xiàn)自己被綠了帚桩。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡嘹黔,死狀恐怖账嚎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤郭蕉,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布疼邀,位于F島的核電站,受9級特大地震影響召锈,放射性物質(zhì)發(fā)生泄漏旁振。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一烟勋、第九天 我趴在偏房一處隱蔽的房頂上張望规求。 院中可真熱鬧,春花似錦卵惦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至较解,卻和暖如春畜疾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背印衔。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工啡捶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人奸焙。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓瞎暑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親与帆。 傳聞我的和親對象是個殘疾皇子了赌,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

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