再也不用擔心問RecycleView了——面試真題詳解

  • 2020 年 11 月 26 日
  • 筆記

關於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和bindView
  • mCacheViews(螢幕外),保存最近移出螢幕的ViewHolder,包含數據和position資訊,復用時必須是相同位置的ViewHolder才能復用,應用場景在那些需要來回滑動的列表中,當往回滑動時,能直接復用ViewHolder數據,不需要重新bindView。
  • mViewCacheExtension(自定義快取),不直接使用,需要用戶自定義實現,默認不實現。
  • mRecyclerPool(快取池),當cacheView滿了後或者adapter被更換,將cacheView中移出的ViewHolder放到Pool中,放之前會把ViewHolder數據清除掉,所以復用時需要重新bindView。

2)四級快取按照順序需要依次讀取。所以完整快取流程是:

  1. 保存快取流程:
  • 插入或是刪除itemView時,先把螢幕內的ViewHolder保存至AttachedScrap
  • 滑動螢幕的時候,先消失的itemview會保存到CacheView,CacheView大小默認是2,超過數量的話按照先入先出原則,移出頭部的itemview保存到RecyclerPool快取池(如果有自定義快取就會保存到自定義快取里),RecyclerPool快取池會按照itemview的itemtype進行保存,每個itemType快取個數為5個,超過就會被回收。
  1. 獲取快取流程:
  • AttachedScrap中獲取,通過pos匹配holder——>獲取失敗,從CacheView中獲取,也是通過pos獲取holder快取
    ——>獲取失敗,從自定義快取中獲取快取——>獲取失敗,從mRecyclerPool中獲取
    ——>獲取失敗,重新創建viewholder——createViewHolder並bindview。

3)了解了快取結構和快取流程,我們再來看看具體的問題
滑動10個,再滑回去,會有幾個執行onBindView?

  • 由之前的快取結構可知,需要重新執行onBindView的只有一種快取區,就是快取池mRecyclerPool

所以我們假設從載入RecyclView開始盤的話(頁面假設可以容納7條數據):

  • 首先,7條數據會依次調用onCreateViewHolderonBindViewHolder
  • 往下滑一條(position=7),那麼會把position=0的數據放到mCacheViews中。此時mCacheViews快取區數量為1,mRecyclerPool數量為0。然後新出現的position=7的數據通過postion在mCacheViews中找不到對應的ViewHolder,通過itemtype也在mRecyclerPool中找不到對應的數據,所以會調用onCreateViewHolderonBindViewHolder方法。
  • 再往下滑一條數據(position=8),如上。
  • 再往下滑一條數據(position=9),position=2的數據會放到mCacheViews中,但是由於mCacheViews快取區默認容量為2,所以position=0的數據會被清空數據然後放到mRecyclerPool快取池中。而新出現的position=9數據由於在mRecyclerPool中還是找不到相應type的ViewHolder,所以還是會走onCreateViewHolderonBindViewHolder方法。所以此時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代替ScrollViewNestedscrollview是官方為了解決滑動衝突問題而設計的新的View。它的定義就是支援嵌套滑動的ScrollView。

所以直接替換成Nestedscrollview就能保證兩者都能正常滑動了。但是要注意設置RecyclerView.setNestedScrollingEnabled(false)這個方法,用來取消RecyclerView本身的滑動效果。

這是因為RecyclerView默認是setNestedScrollingEnabled(true),這個方法的含義是支援嵌套滾動的。也就是說當它嵌套在NestedScrollView中時,默認會隨著NestedScrollView滾動而滾動,放棄了自己的滾動。所以給我們的感覺就是滯留、卡頓。所以我們將它設置為false就解決了卡頓問題,讓他正常的滑動,不受外部影響。

拜拜

今天聊了不少,關於RecycleView重要的知識點應該都涉及到了,其中bindview的問題下次有機會我會再詳細的說一下,配合圖片日誌。

最後希望大家好好鞏固知識,加油。

拜拜

有一起學習的小夥伴可以關注下❤️我的公眾號——碼上積木,每天剖析一個知識點,我們一起積累知識。