RecyclerView 復(fù)用錯(cuò)亂通用解法

RecyclerView 復(fù)用錯(cuò)亂通用解法

本文原創(chuàng),轉(zhuǎn)載請(qǐng)注明出處乖酬。
歡迎關(guān)注我的 簡(jiǎn)書 ,關(guān)注我的專題 Android Class 我會(huì)長(zhǎng)期堅(jiān)持為大家收錄簡(jiǎn)書上高質(zhì)量的 Android 相關(guān)博文。

寫在前面:
在上篇文章中說(shuō)過(guò)對(duì)于像 RecyclerView 或者 ListView 等等此類在有限屏幕中展示大量?jī)?nèi)容的控件,復(fù)用的邏輯就是其核心的邏輯呕诉,而關(guān)于復(fù)用導(dǎo)致最常見的 bug 就是復(fù)用錯(cuò)亂。在大上周我就遇到了一個(gè)很奇怪的問(wèn)題吃度,這也是我下決心研究 RecyclerView 的原因甩挫。

RecyclerView 源碼分析

而這篇文章的目的首先是討論在 RecyclerView 復(fù)用錯(cuò)亂時(shí),一些通用的解決思路椿每,其次就是探究我遇到的那個(gè)奇怪的問(wèn)題伊者,幫助未來(lái)同樣遇到的朋友們。

復(fù)用錯(cuò)亂的解決辦法

本文的前半部分很簡(jiǎn)單的间护,以為關(guān)于復(fù)用錯(cuò)亂亦渗,RecyclerView 已經(jīng)有他的前輩 ListView 替它踩了很多坑了。雖然他們的復(fù)用邏輯是有差異的汁尺,例如 ListView 只有兩層緩存法精,但是 RecyclerView 可以理解為有四層;ListView 緩存的單位是 view痴突,而 RecyclerView 緩存的單位是 ViewHolder搂蜓。但是不管他們復(fù)用邏輯的差異如何,終歸都是把那個(gè)緩存起來(lái)的 view 拿過(guò)來(lái)接著用辽装,所以解決復(fù)用錯(cuò)亂的方法是一樣的帮碰。

RecyclerView 復(fù)用導(dǎo)致錯(cuò)亂的原因其實(shí)就是拿出來(lái)之前的 View 來(lái)添加到新 item 上,之前 View 的狀態(tài)一直保留著如迟,所以也就錯(cuò)亂了收毫。不過(guò)解決起來(lái)很簡(jiǎn)單:

首先我們以 adapter 數(shù)據(jù)的來(lái)源分為兩大類:

1.當(dāng)數(shù)據(jù)來(lái)源是同步的

這種情況是最簡(jiǎn)單的,你就保證當(dāng) onBindViewHolder 方法調(diào)用的時(shí)候殷勘,你的 itemview 中每個(gè) view 的狀態(tài)都有一個(gè)默認(rèn)值此再。這是什么意思呢?

        if ("<unknown>".equals(artists)) {
            holder.cbMusicState.setChecked(true);
        } else {
            holder.cbMusicState.setChecked(false);
        }

假設(shè)我們的 holder 里面有個(gè) Checkbox 控件玲销,當(dāng)歌手名為 unknown 時(shí)输拇,Checkbox 勾選。注意個(gè)時(shí)候你一定要加上這個(gè) else 條件贤斜,才能保證復(fù)用這個(gè) ViewHolder 的時(shí)候策吠,Checkbox 的狀態(tài)不出錯(cuò)。任何控件都一樣瘩绒,總結(jié)起來(lái)就是你要給每個(gè)控件的狀態(tài)賦一個(gè)新的值猴抹,替換掉之前的,這樣自然不會(huì)出現(xiàn)什么復(fù)用錯(cuò)亂的問(wèn)題锁荔。

2.當(dāng)數(shù)據(jù)的來(lái)源是異步的

這種情況也很常見蟀给,我們舉個(gè)栗子,比如你的 ItemView 里面有個(gè) ImageView,每次 onBindViewHolder 的時(shí)候跋理,你傳入一個(gè) url择克,等待服務(wù)器返回的結(jié)果,然后展示在 ImageView 上前普。這種情況會(huì)怎樣導(dǎo)致錯(cuò)亂呢肚邢?

是這樣的,假設(shè)我進(jìn)入了頁(yè)面拭卿,開始為第一個(gè) ImageView 請(qǐng)求圖片骡湖,但是此刻我下劃屏幕,劃到了第四個(gè) item峻厚,此時(shí)第一個(gè) item 已經(jīng)不可見了勺鸦,第四個(gè) item 復(fù)用了第一個(gè) item 的 imageview,恰好此刻第一個(gè) imageview 的圖片結(jié)果返回了目木,就正好展示在了第四個(gè) itemview 上。 這樣就發(fā)生了圖片的錯(cuò)亂懊渡。

出現(xiàn)這個(gè)問(wèn)題的原因就是這個(gè) ImageView 和請(qǐng)求的 url 沒(méi)一一綁定刽射,所以按照這個(gè)思路來(lái)解決吧:

    holder.ivCameraImages.setBackground(R.drawable.place_holder);
    
    holder.ivCameraImages.setTag(imageURL);

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == MSG_IMAGE) {
                Bitmap bm = (Bitmap) msg.obj;
                if (bm != null) {
                    if (TextUtils.equals((String) imageView.getTag(), imageURL)) {
                        imageView.setBackground(new BitmapDrawable(bm));
                    }
                }
            }
        }

首先在沒(méi)加載圖片之前,給 ImageView 設(shè)置一個(gè)默認(rèn)圖片剃执,然后通過(guò) setTag 方法誓禁,將 ImageView 和 圖片的 url 一一對(duì)應(yīng)起來(lái),設(shè)置的時(shí)候再判斷一下肾档,這個(gè) imageview 的 tag 和當(dāng)時(shí)請(qǐng)求的 url摹恰,是不是一致的,如果是一致的怒见,再保存俗慈。

以上就是復(fù)用錯(cuò)亂時(shí)兩種比較通用的解法,基本上可以覆蓋大部分情況遣耍。

一個(gè)奇怪的問(wèn)題

這個(gè)問(wèn)題的現(xiàn)象是這樣子的:

當(dāng) RecyclerView 的條目很少的時(shí)候闺阱,比如只有六個(gè),將 RecyclerView 從上滑動(dòng)到下舵变,這個(gè)時(shí)候是正常的酣溃,onBindViewHolder 會(huì)調(diào)用,不過(guò)此時(shí)從底部上劃的時(shí)候纪隙,上方的 item 從不可見到可見的這個(gè)過(guò)程中赊豌,onBindViewHolder 并沒(méi)有調(diào)用,這個(gè)時(shí)候我也就沒(méi)辦法進(jìn)行一些刷新 item 的操作了绵咱。

這個(gè)問(wèn)題的原因是 onBindViewHolder 方法不調(diào)用導(dǎo)致的碘饼,我在 StackOverflow 上搜索了很多答案,終于找到了一個(gè)可以解決我的問(wèn)題的:

recyclerview-not-recycling-views-if-the-view-count-is-small

(中文資料壓根就沒(méi)有,所以掌握英文搜索是多么的重要)

你可以調(diào)用

recyclerView.setItemViewCacheSize(int);

這個(gè) api派昧,去調(diào)整 RecyclerView 的復(fù)用邏輯和方式來(lái)解決 onBindViewHolder 沒(méi)有調(diào)用的這個(gè)問(wèn)題黔姜。

但是原理是怎樣的呢?作為一名好奇心頗重的程序員蒂萎,一步步 debug RecyclerView 的源代碼秆吵,發(fā)現(xiàn)了導(dǎo)致這個(gè)問(wèn)題的原因,一起來(lái)看看吧五慈。

在上一篇文章中纳寂,我們分析了 RecyclerView 的源碼,其中復(fù)用邏輯的模塊泻拦,有一個(gè)非常重要的核心方法 tryBindViewHolderByDeadline毙芜,這個(gè)方法目的就是在 RecyclerView 的層層緩存結(jié)構(gòu)中,取出 ViewHolder争拐。

這里就不再次研究它了腋粥,想了解的去看之前的文章,我來(lái)描述一下對(duì)于這個(gè)場(chǎng)景架曹,簡(jiǎn)化之后的邏輯:

當(dāng) RecyclerView 從底部向上滑動(dòng)的時(shí)候隘冲,會(huì)先后從 mCachedViews 和 mRecyclerPool 中尋找緩存的 ViewHolder。

mCachedViews 和 mRecyclerPool 之間又有什么關(guān)系呢绑雄?

        public void setViewCacheSize(int viewCount) {
            mRequestedCacheMax = viewCount;
            updateViewCacheSize();
        }

        void updateViewCacheSize() {
            int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
            mViewCacheMax = mRequestedCacheMax + extraCache;

            // first, try the views that can be recycled
            for (int i = mCachedViews.size() - 1;
                    i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
                recycleCachedViewAt(i);
            }
        }

當(dāng)調(diào)用 setViewCacheSize 這個(gè)方法時(shí)展辞,相當(dāng)于是給 mViewCacheMax 這個(gè)變量賦值了, for 循環(huán)調(diào)用 recycleCachedViewAt 的作用是將 mCachedViews 中緩存的 ViewHolder 放進(jìn) RecyclerPool 中万牺÷拚洌可以看到 for 循環(huán)的周期是從 mCachedViews 的最后一個(gè)對(duì)象直到 mCachedViews.size == mViewCacheMax 這個(gè)值時(shí)。

也就是可以這么理解脚粟, setViewCacheSize 這個(gè)方法其實(shí)就是為 mCachedViews 集合設(shè)置所能持有 ViewHolder 的最大數(shù)量覆旱。

當(dāng) setViewCacheSize(0)時(shí),RecyclerView 想去復(fù)用 ViewHolder 時(shí)核无,只能去 RecyclerPool 中去取了通殃,這里就有問(wèn)題來(lái)了,從 RecyclerPool 中取和從 mCachedViews 中取 ViewHolder 中又有什么區(qū)別呢厕宗?

                if (holder == null) { // fallback to pool
                    if (DEBUG) {
                        Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                                + position + ") fetching from shared pool");
                    }
                    holder = getRecycledViewPool().getRecycledView(type);
                    if (holder != null) {
                        holder.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
                            invalidateDisplayListInt(holder);
                        }
                    }
                }

當(dāng)從 RecyclerPool 取出 ViewHolder 時(shí)画舌,調(diào)用了 resetInternal 這個(gè)函數(shù)的作用是清空一些記錄的參數(shù),包括之前記錄 ViewHolder 狀態(tài)的 mFlags已慢。

 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);
                }
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }

代碼再往下走的時(shí)候曲聂,剛剛清空的 flag 參數(shù)這個(gè)時(shí)候就用到了,holder.isBound() 返回 flase佑惠,進(jìn)入 if 判斷朋腋,調(diào)用 tryBindViewHolderByDeadline 進(jìn)而調(diào)用了 onBindViewHolder齐疙。

到這里這個(gè)邏輯就描述清楚了,所以設(shè)置 setViewCacheSize 來(lái)調(diào)整 mCachedViews 保存 ViewHolder 的大小旭咽,就能解決問(wèn)題贞奋。

當(dāng)然有些特殊的情況,某些位置就不能調(diào)用 onBindViewHolder穷绵,沒(méi)關(guān)系轿塔,可以監(jiān)聽 RecyclerView 的滑動(dòng),當(dāng)滑動(dòng)停止的時(shí)候仲墨,再調(diào)用 notify 刷新下列表也是可以的勾缭。

好了本文到這里就結(jié)束了~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市目养,隨后出現(xiàn)的幾起案子俩由,更是在濱河造成了極大的恐慌,老刑警劉巖癌蚁,帶你破解...
    沈念sama閱讀 206,013評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件幻梯,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡努释,警方通過(guò)查閱死者的電腦和手機(jī)礼旅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)洽洁,“玉大人,你說(shuō)我怎么就攤上這事菲嘴《鲎裕” “怎么了?”我有些...
    開封第一講書人閱讀 152,370評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵龄坪,是天一觀的道長(zhǎng)昭雌。 經(jīng)常有香客問(wèn)我,道長(zhǎng)健田,這世上最難降的妖魔是什么烛卧? 我笑而不...
    開封第一講書人閱讀 55,168評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮妓局,結(jié)果婚禮上总放,老公的妹妹穿的比我還像新娘。我一直安慰自己好爬,他們只是感情好局雄,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著存炮,像睡著了一般炬搭。 火紅的嫁衣襯著肌膚如雪蜈漓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,954評(píng)論 1 283
  • 那天宫盔,我揣著相機(jī)與錄音融虽,去河邊找鬼。 笑死灼芭,一個(gè)胖子當(dāng)著我的面吹牛有额,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播姿鸿,決...
    沈念sama閱讀 38,271評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼谆吴,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了苛预?” 一聲冷哼從身側(cè)響起句狼,我...
    開封第一講書人閱讀 36,916評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎热某,沒(méi)想到半個(gè)月后腻菇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,382評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡昔馋,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評(píng)論 2 323
  • 正文 我和宋清朗相戀三年筹吐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秘遏。...
    茶點(diǎn)故事閱讀 37,989評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡丘薛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出邦危,到底是詐尸還是另有隱情洋侨,我是刑警寧澤,帶...
    沈念sama閱讀 33,624評(píng)論 4 322
  • 正文 年R本政府宣布倦蚪,位于F島的核電站希坚,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏陵且。R本人自食惡果不足惜裁僧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望慕购。 院中可真熱鬧聊疲,春花似錦、人聲如沸沪悲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)可训。三九已至昌妹,卻和暖如春捶枢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背飞崖。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工烂叔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人固歪。 一個(gè)月前我還...
    沈念sama閱讀 45,401評(píng)論 2 352
  • 正文 我出身青樓蒜鸡,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親牢裳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子逢防,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評(píng)論 2 345

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