天天看點

RecyclerView 複用錯亂通用解法RecyclerView 複用錯亂通用解法

RecyclerView 複用錯亂通用解法

寫在前面:

在上篇文章中說過對于像 RecyclerView 或者 ListView 等等此類在有限螢幕中展示大量内容的控件,複用的邏輯就是其核心的邏輯,而關于複用導緻最常見的 bug 就是複用錯亂。在大上周我就遇到了一個很奇怪的問題,這也是我下決心研究 RecyclerView 的原因。

RecyclerView 源碼分析

而這篇文章的目的首先是讨論在 RecyclerView 複用錯亂時,一些通用的解決思路,其次就是探究我遇到的那個奇怪的問題,幫助未來同樣遇到的朋友們。

複用錯亂的解決辦法

本文的前半部分很簡單的,以為關于複用錯亂,RecyclerView 已經有他的前輩 ListView 替它踩了很多坑了。雖然他們的複用邏輯是有差異的,例如 ListView 隻有兩層緩存,但是 RecyclerView 可以了解為有四層;ListView 緩存的機關是 view,而 RecyclerView 緩存的機關是 ViewHolder。但是不管他們複用邏輯的差異如何,終歸都是把那個緩存起來的 view 拿過來接着用,是以解決複用錯亂的方法是一樣的。

RecyclerView 複用導緻錯亂的原因其實就是拿出來之前的 View 來添加到新 item 上,之前 View 的狀态一直保留着,是以也就錯亂了。不過解決起來很簡單:

首先我們以 adapter 資料的來源分為兩大類:

 1.當資料來源是同步的

這種情況是最簡單的,你就保證當

onBindViewHolder

方法調用的時候,你的 itemview 中每個 view 的狀态都有一個預設值。這是什麼意思呢?

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

假設我們的 holder 裡面有個 Checkbox 控件,當歌手名為 unknown 時,Checkbox 勾選。注意個時候你一定要加上這個 else 條件,才能保證複用這個 ViewHolder 的時候,Checkbox 的狀态不出錯。任何控件都一樣,總結起來就是你要給每個控件的狀态賦一個新的值,替換掉之前的,這樣自然不會出現什麼複用錯亂的問題。

 2.當資料的來源是異步的

這種情況也很常見,我們舉個栗子,比如你的 ItemView 裡面有個 ImageView,每次

onBindViewHolder

的時候,你傳入一個 url,等待伺服器傳回的結果,然後展示在 ImageView 上。這種情況會怎樣導緻錯亂呢?

是這樣的,假設我進入了頁面,開始為第一個 ImageView 請求圖檔,但是此刻我下劃螢幕,劃到了第四個 item,此時第一個 item 已經不可見了,第四個 item 複用了第一個 item 的 imageview,恰好此刻第一個 imageview 的圖檔結果傳回了,就正好展示在了第四個 itemview 上。 這樣就發生了圖檔的錯亂。

出現這個問題的原因就是這個 ImageView 和請求的 url 沒一一綁定,是以按照這個思路來解決吧:

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

首先在沒加載圖檔之前,給 ImageView 設定一個預設圖檔,然後通過 setTag 方法,将 ImageView 和 圖檔的 url 一一對應起來,設定的時候再判斷一下,這個 imageview 的 tag 和當時請求的 url,是不是一緻的,如果是一緻的,再儲存。

以上就是複用錯亂時兩種比較通用的解法,基本上可以覆寫大部分情況。

一個奇怪的問題

這個問題的現象是這樣子的:

當 RecyclerView 的條目很少的時候,比如隻有六個,将 RecyclerView 從上滑動到下,這個時候是正常的,

onBindViewHolder

會調用,不過此時從底部上劃的時候,上方的 item 從不可見到可見的這個過程中,

onBindViewHolder

并沒有調用,這個時候我也就沒辦法進行一些重新整理 item 的操作了。

這個問題的原因是

onBindViewHolder

方法不調用導緻的,我在 StackOverflow 上搜尋了很多答案,終于找到了一個可以解決我的問題的:

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

(中文資料壓根就沒有,是以掌握英文搜尋是多麼的重要)

你可以調用

recyclerView.setItemViewCacheSize(int);
           

這個 api,去調整 RecyclerView 的複用邏輯和方式來解決

onBindViewHolder

沒有調用的這個問題。

但是原理是怎樣的呢?作為一名好奇心頗重的程式員,一步步 debug RecyclerView 的源代碼,發現了導緻這個問題的原因,一起來看看吧。

在上一篇文章中,我們分析了 RecyclerView 的源碼,其中複用邏輯的子產品,有一個非常重要的核心方法

tryBindViewHolderByDeadline

,這個方法目的就是在 RecyclerView 的層層緩存結構中,取出 ViewHolder。

這裡就不再次研究它了,想了解的去看之前的文章,我來描述一下對于這個場景,簡化之後的邏輯:

當 RecyclerView 從底部向上滑動的時候,會先後從 mCachedViews 和 mRecyclerPool 中尋找緩存的 ViewHolder。

mCachedViews 和 mRecyclerPool 之間又有什麼關系呢?

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

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

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

當調用

setViewCacheSize

這個方法時,相當于是給 mViewCacheMax 這個變量指派了, for 循環調用 recycleCachedViewAt 的作用是将 mCachedViews 中緩存的 ViewHolder 放進 RecyclerPool 中。可以看到 for 循環的周期是從 mCachedViews 的最後一個對象直到 mCachedViews.size == mViewCacheMax 這個值時。

也就是可以這麼了解,

setViewCacheSize

這個方法其實就是為 mCachedViews 集合設定所能持有 ViewHolder 的最大數量。

setViewCacheSize(0)

時,RecyclerView 想去複用 ViewHolder 時,隻能去 RecyclerPool 中去取了,這裡就有問題來了,從 RecyclerPool 中取和從 mCachedViews 中取 ViewHolder 中又有什麼差別呢?

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

當從 RecyclerPool 取出 ViewHolder 時,調用了

resetInternal

這個函數的作用是清空一些記錄的參數,包括之前記錄 ViewHolder 狀态的 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);
            }
           

代碼再往下走的時候,剛剛清空的 flag 參數這個時候就用到了,

holder.isBound()

傳回 flase,進入 if 判斷,調用

tryBindViewHolderByDeadline

進而調用了

onBindViewHolder

到這裡這個邏輯就描述清楚了,是以設定 setViewCacheSize 來調整 mCachedViews 儲存 ViewHolder 的大小,就能解決問題。

當然有些特殊的情況,某些位置就不能調用

onBindViewHolder

,沒關系,可以監聽 RecyclerView 的滑動,當滑動停止的時候,再調用 notify 重新整理下清單也是可以的。

好了本文到這裡就結束了~

繼續閱讀