關於RecycleView,之前我寫過一篇比較基礎的文章,主要說的是快取和優化等問題。但是有讀者反映問題不夠實際和深入。於是,我又去淘了一些關於RecycleView的面試真題,大家一起看看吧,這次的問題如果都弄懂了,下次面試再遇到RecycleView應該就沒啥可擔心的了。
- 講一下
RecyclerView
的快取機制,滑動10個,再滑回去,會有幾個執行onBindView
。快取的是什麼?cachedView
會執行onBindView嗎? RecyclerView
預取機制- 如何實現
RecyclerView
的區域性更新,用過payload
嗎,notifyItemChange方法中的引數? RecyclerView
巢狀RecyclerView
滑動衝突,NestScrollView巢狀RecyclerView。
講一下RecyclerView的快取機制,滑動10個,再滑回去,會有幾個執行onBindView。快取的是什麼?cachedView會執行onBindView嗎?
RecyclerView預取機制
這兩個問題都是關於快取的,我就一起說了。
1)首先說下RecycleView的快取結構:
Recycleview有四級快取,分別是mAttachedScrap(螢幕內),mCacheViews(螢幕外),mViewCacheExtension(自定義快取),mRecyclerPool(快取池)
mAttachedScrap(螢幕內)
,用於螢幕內itemview快速重用,不需要重新createView和bindViewmCacheViews(螢幕外)
,儲存最近移出螢幕的ViewHolder,包含資料和position資訊,複用時必須是相同位置的ViewHolder才能複用,應用場景在那些需要來回滑動的列表中,當往回滑動時,能直接複用ViewHolder資料,不需要重新bindView。mViewCacheExtension(自定義快取)
,不直接使用,需要使用者自定義實現,預設不實現。mRecyclerPool(快取池)
,當cacheView滿了後或者adapter被更換,將cacheView中移出的ViewHolder放到Pool中,放之前會把ViewHolder資料清除掉,所以複用時需要重新bindView。
2)四級快取按照順序需要依次讀取。所以完整快取流程是:
- 儲存快取流程:
- 插入或是刪除
itemView
時,先把螢幕內的ViewHolder儲存至AttachedScrap
中 - 滑動螢幕的時候,先消失的itemview會儲存到
CacheView
,CacheView大小預設是2,超過數量的話按照先入先出原則,移出頭部的itemview儲存到RecyclerPool快取池
(如果有自定義快取就會儲存到自定義快取裡),RecyclerPool快取池會按照itemview的itemtype
進行儲存,每個itemType快取個數為5個,超過就會被回收。
- 獲取快取流程:
- AttachedScrap中獲取,通過pos匹配holder——>獲取失敗,從
CacheView
中獲取,也是通過pos獲取holder快取
——>獲取失敗,從自定義快取
中獲取快取——>獲取失敗,從mRecyclerPool
中獲取
——>獲取失敗,重新建立viewholder
——createViewHolder並bindview。
3)瞭解了快取結構和快取流程,我們再來看看具體的問題
滑動10個,再滑回去,會有幾個執行onBindView?
- 由之前的快取結構可知,需要重新執行
onBindView
的只有一種快取區,就是快取池mRecyclerPool
。
所以我們假設從載入RecyclView
開始盤的話(頁面假設可以容納7條資料):
- 首先,7條資料會依次呼叫
onCreateViewHolder
和onBindViewHolder
。 - 往下滑一條(position=7),那麼會把position=0的資料放到
mCacheViews
中。此時mCacheViews
快取區數量為1,mRecyclerPool
數量為0。然後新出現的position=7的資料通過postion在mCacheViews
中找不到對應的ViewHolder
,通過itemtype
也在mRecyclerPool
中找不到對應的資料,所以會呼叫onCreateViewHolder
和onBindViewHolder
方法。 - 再往下滑一條資料(position=8),如上。
- 再往下滑一條資料(position=9),position=2的資料會放到
mCacheViews
中,但是由於mCacheViews
快取區預設容量為2,所以position=0的資料會被清空資料然後放到mRecyclerPool
快取池中。而新出現的position=9資料由於在mRecyclerPool
中還是找不到相應type的ViewHolder,所以還是會走onCreateViewHolder
和onBindViewHolder
方法。所以此時mCacheViews
快取區數量為2,mRecyclerPool
數量為1。 - 再往下滑一條資料(position=10),這時候由於可以在
mRecyclerPool
中找到相同viewtype的ViewHolder了。所以就直接複用了,並呼叫onBindViewHolder
方法繫結資料。 - 後面依次類推,剛消失的兩條資料會被放到
mCacheViews
中,再出現的時候是不會呼叫onBindViewHolder方法,而複用的第三條資料是從mRecyclerPool
中取得,就會呼叫onBindViewHolder
方法了。
4)所以這個問題就得出結論了(假設mCacheViews
容量為預設值2):
-
如果一開始滑動的是新資料,那麼滑動10個,就會走10個
bindview
方法。然後滑回去,會走10-2個bindview
方法。一共18次呼叫。 -
如果一開始滑動的是老資料,那麼滑動10-2個,就會走8個
bindview
方法。然後滑回去,會走10-2個bindview
方法。一共16次呼叫。
但是但是,實際情況又有點不一樣。因為Recycleview
在v25版本引入了一個新的機制,預取機制
。
預取機制
,就是在滑動過程中,會把將要展示的一個元素提前快取到mCachedViews
中,所以滑動10個元素的時候,第11個元素也會被建立,也就多走了一次bindview
方法。但是滑回去的時候不影響,因為就算提前取了一個快取資料,只是把bindview
方法提前了,並不影響總的繫結item數量。
所以滑動的是新資料的情況下就會多一次呼叫bindview
方法。
5)總結,問題怎麼答呢?
- 四級快取和流程說一下。
- 滑動10個,再滑回去,
bindview
可以是19次呼叫,可以是16次呼叫。 - 快取的其實就是快取item的view,在Recycleview中就是
viewholder
。 cachedView
就是mCacheViews
快取區中的view,是不需要重新繫結資料的。
如何實現RecyclerView的區域性更新,用過payload嗎,notifyItemChange方法中的引數?
關於RecycleView的資料更新,主要有以下幾個方法:
notifyDataSetChanged()
,重新整理全部可見的item。
*notifyItemChanged(int)
,重新整理指定item。notifyItemRangeChanged(int,int)
,從指定位置開始重新整理指定個item。notifyItemInserted(int)、notifyItemMoved(int)、notifyItemRemoved(int)
。插入、移動一個並自動重新整理。notifyItemChanged(int, Object)
,區域性重新整理。
可以看到,關於view的區域性重新整理就是notifyItemChanged(int, Object)方法,下面具體說說:
notifyItemChange
有兩個構造方法:
- notifyItemChanged(int position, @Nullable Object payload)
- notifyItemChanged(int position)
其中payload
引數可以認為是你要重新整理的一個標示,比如我有時候只想重新整理itemView
中的textview
,有時候只想重新整理imageview
?又或者我只想某一個view的文字顏色進行高亮設定?那麼我就可以通過payload
引數來標示這個特殊的需求了。
具體怎麼做呢?比如我呼叫了notifyItemChanged(14,"changeColor")
,那麼在onBindViewHolder
回撥方法中做下判斷即可:
@Override
public void onBindViewHolder(ViewHolderholder, int position, List<Object> payloads) {
if (payloads.isEmpty()) {
// payloads為空,說明是更新整個ViewHolder
onBindViewHolder(holder, position);
} else {
// payloads不為空,這隻更新需要更新的View即可。
String payload = payloads.get(0).toString();
if ("changeColor".equals(payload)) {
holder.textView.setTextColor("");
}
}
}
RecyclerView巢狀RecyclerView滑動衝突,NestScrollView巢狀RecyclerView。
1)RecyclerView
巢狀RecyclerView
的情況下,如果兩者都要上下滑動,那麼就會引起滑動衝突。預設情況下外層的RecycleView可滑,內層不可滑。
之前說過解決滑動衝突的辦法有兩種:內部攔截法和外部攔截法。
這裡我提供一種內部攔截法,還有一些其他的辦法大家可以自己思考下。
holder.recyclerView.setOnTouchListener { v, event ->
when(event.action){
//當按下操作的時候,就通知父view不要攔截,拿起操作就設定可以攔截,正常走父view的滑動。
MotionEvent.ACTION_DOWN,MotionEvent.ACTION_MOVE -> v.parent.requestDisallowInterceptTouchEvent(true)
MotionEvent.ACTION_UP -> v.parent.requestDisallowInterceptTouchEvent(false)
}
false}
2)關於ScrclerView
的滑動衝突還是同樣的解決辦法,就是進行事件攔截。
還有一個辦法就是用Nestedscrollview
代替ScrollView
,Nestedscrollview
是官方為了解決滑動衝突問題而設計的新的View。它的定義就是支援巢狀滑動的ScrollView。
所以直接替換成Nestedscrollview
就能保證兩者都能正常滑動了。但是要注意設定RecyclerView.setNestedScrollingEnabled(false)
這個方法,用來取消RecyclerView本身的滑動效果。
這是因為RecyclerView預設是setNestedScrollingEnabled(true)
,這個方法的含義是支援巢狀滾動的。也就是說當它巢狀在NestedScrollView
中時,預設會隨著NestedScrollView
滾動而滾動,放棄了自己的滾動。所以給我們的感覺就是滯留、卡頓。所以我們將它設定為false就解決了卡頓問題,讓他正常的滑動,不受外部影響。
拜拜
今天聊了不少,關於RecycleView重要的知識點應該都涉及到了,其中bindview
的問題下次有機會我會再詳細的說一下,配合圖片日誌。
最後希望大家好好鞏固知識,加油。
拜拜
有一起學習的小夥伴可以關注下❤️我的公眾號——碼上積木,每天剖析一個知識點,我們一起積累知識。