Android RecyclerView 復(fù)用錯(cuò)亂通用解法詳解
寫(xiě)在前面:
在上篇文章中說(shuō)過(guò)對(duì)于像 RecyclerView 或者 ListView 等等此類(lèi)在有限屏幕中展示大量?jī)?nèi)容的控件,復(fù)用的邏輯就是其核心的邏輯,而關(guān)于復(fù)用導(dǎo)致最常見(jiàn)的 bug 就是復(fù)用錯(cuò)亂。在大上周我就遇到了一個(gè)很奇怪的問(wèn)題,這也是我下決心研究 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)源分為兩大類(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)源是異步的
這種情況也很常見(jiàn),我們舉個(gè)栗子,比如你的 ItemView 里面有個(gè) ImageView,每次 onBindViewHolder
的時(shí)候,你傳入一個(gè) url,等待服務(wù)器返回的結(jié)果,然后展示在 ImageView 上。這種情況會(huì)怎樣導(dǎo)致錯(cuò)亂呢?
是這樣的,假設(shè)我進(jìn)入了頁(yè)面,開(kāi)始為第一個(gè) ImageView 請(qǐng)求圖片,但是此刻我下劃屏幕,劃到了第四個(gè) item,此時(shí)第一個(gè) item 已經(jīng)不可見(jiàn)了,第四個(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 從不可見(jiàn)到可見(jiàn)的這個(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)聽(tīng) RecyclerView 的滑動(dòng),當(dāng)滑動(dòng)停止的時(shí)候,再調(diào)用 notify 刷新下列表也是可以的。
好了本文到這里就結(jié)束了~希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android中使用RecyclerView實(shí)現(xiàn)下拉刷新和上拉加載
- Android中RecyclerView布局代替GridView實(shí)現(xiàn)類(lèi)似支付寶的界面
- Android中RecyclerView實(shí)現(xiàn)多級(jí)折疊列表效果(二)
- Android中RecyclerView實(shí)現(xiàn)橫向滑動(dòng)代碼
- Android RecyclerView網(wǎng)格布局(支持多種分割線)詳解(2)
- Android Recyclerview實(shí)現(xiàn)多選,單選,全選,反選,批量刪除的功能
- Android RecyclerView實(shí)現(xiàn)下拉刷新和上拉加載
- Android RecyclerView的卡頓問(wèn)題的解決方法
- Android項(xiàng)目實(shí)戰(zhàn)之仿網(wǎng)易新聞的頁(yè)面(RecyclerView )
- Android如何利用RecyclerView實(shí)現(xiàn)列表倒計(jì)時(shí)效果實(shí)例代碼
相關(guān)文章
Android:Field can be converted to a local varible.的解決辦法
這篇文章主要介紹了Android:Field can be converted to a local varible.的解決辦法的相關(guān)資料,希望通過(guò)本文能幫助到大家,讓大家遇到這樣的問(wèn)題輕松解決,需要的朋友可以參考下2017-10-10android基礎(chǔ)總結(jié)篇之八:創(chuàng)建及調(diào)用自己的ContentProvider
這篇文章主要介紹了android基礎(chǔ)總結(jié)篇之八:創(chuàng)建及調(diào)用自己的ContentProvider,有興趣的可以了解一下。2016-11-11Android 實(shí)現(xiàn)釘釘自動(dòng)打卡功能
這篇文章主要介紹了Android 實(shí)現(xiàn)釘釘自動(dòng)打卡功能的步驟,幫助大家更好的理解和學(xué)習(xí)使用Android,感興趣的朋友可以了解下2021-03-03Android開(kāi)發(fā)設(shè)計(jì)nowinandroid構(gòu)建腳本學(xué)習(xí)
這篇文章主要為大家介紹了Android開(kāi)發(fā)設(shè)計(jì)nowinandroid構(gòu)建腳本學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11flutter實(shí)現(xiàn)底部導(dǎo)航欄切換
這篇文章主要為大家詳細(xì)介紹了flutter實(shí)現(xiàn)底部導(dǎo)航欄切換,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07Android實(shí)現(xiàn)自動(dòng)填寫(xiě)獲取驗(yàn)證碼功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)自動(dòng)填寫(xiě)獲取驗(yàn)證碼功能,感興趣的小伙伴們可以參考一下2016-03-03詳解Android中用于線程處理的AsyncTask類(lèi)的用法及源碼
這篇文章主要介紹了Android中用于線程處理的AsyncTask類(lèi)的用法及源碼,講到了實(shí)現(xiàn)AsyncTask中所用到的Handler及線程池等要點(diǎn),需要的朋友可以參考下2016-05-05淺析Android Service中實(shí)現(xiàn)彈出對(duì)話框的坑
這篇文章主要介紹了Android Service中實(shí)現(xiàn)彈出對(duì)話框的坑,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04Android中Listview點(diǎn)贊功能的實(shí)現(xiàn)
最近一段時(shí)間在研究android方面的知識(shí),利用listview實(shí)現(xiàn)點(diǎn)贊功能,下面小編通過(guò)本文給大家介紹下基本思路,需要的朋友可以參考下2016-11-11