再也不用擔(dān)心面試問(wèn)RecyclerView了

嗨,大家好,最近去淘了一些關(guān)于RecyclerView的面試真題,大家一起看看吧,這次的問(wèn)題如果都弄懂了,下次面試再遇到RecyclerView應(yīng)該就沒(méi)啥可擔(dān)心的了。

  • 講一下RecyclerView的緩存機(jī)制,滑動(dòng)10個(gè),再滑回去,會(huì)有幾個(gè)執(zhí)行onBindView。緩存的是什么?cachedView會(huì)執(zhí)行onBindView嗎?
  • RecyclerView預(yù)取機(jī)制
  • 如何實(shí)現(xiàn)RecyclerView的局部更新,用過(guò)payload嗎,notifyItemChange方法中的參數(shù)?
  • RecyclerView嵌套RecyclerView滑動(dòng)沖突,NestScrollView嵌套R(shí)ecyclerView。
  • 說(shuō)說(shuō)RecyclerView性能優(yōu)化。

講一下RecyclerView的緩存機(jī)制,滑動(dòng)10個(gè),再滑回去,會(huì)有幾個(gè)執(zhí)行onBindView。緩存的是什么?cachedView會(huì)執(zhí)行onBindView嗎?

RecyclerView預(yù)取機(jī)制

這兩個(gè)問(wèn)題都是關(guān)于緩存的,我就一起說(shuō)了。

1)首先說(shuō)下RecyclerView的緩存結(jié)構(gòu):

Recyclerview有四級(jí)緩存,分別是mAttachedScrap(屏幕內(nèi)),mCacheViews(屏幕外),mViewCacheExtension(自定義緩存),mRecyclerPool(緩存池)

  • mAttachedScrap(屏幕內(nèi)),用于屏幕內(nèi)itemview快速重用,不需要重新createView和bindView
  • mCacheViews(屏幕外),保存最近移出屏幕的ViewHolder,包含數(shù)據(jù)和position信息,復(fù)用時(shí)必須是相同位置的ViewHolder才能復(fù)用,應(yīng)用場(chǎng)景在那些需要來(lái)回滑動(dòng)的列表中,當(dāng)往回滑動(dòng)時(shí),能直接復(fù)用ViewHolder數(shù)據(jù),不需要重新bindView。
  • mViewCacheExtension(自定義緩存),不直接使用,需要用戶自定義實(shí)現(xiàn),默認(rèn)不實(shí)現(xiàn)。
  • mRecyclerPool(緩存池),當(dāng)cacheView滿了后或者adapter被更換,將cacheView中移出的ViewHolder放到Pool中,放之前會(huì)把ViewHolder數(shù)據(jù)清除掉,所以復(fù)用時(shí)需要重新bindView。

2)四級(jí)緩存按照順序需要依次讀取。所以完整緩存流程是:

  1. 保存緩存流程:
  • 插入或是刪除itemView時(shí),先把屏幕內(nèi)的ViewHolder保存至AttachedScrap
  • 滑動(dòng)屏幕的時(shí)候,先消失的itemview會(huì)保存到CacheView,CacheView大小默認(rèn)是2,超過(guò)數(shù)量的話按照先入先出原則,移出頭部的itemview保存到RecyclerPool緩存池(如果有自定義緩存就會(huì)保存到自定義緩存里),RecyclerPool緩存池會(huì)按照itemview的itemtype進(jìn)行保存,每個(gè)itemType緩存?zhèn)€數(shù)為5個(gè),超過(guò)就會(huì)被回收。
  1. 獲取緩存流程:
  • AttachedScrap中獲取,通過(guò)pos匹配holder——>獲取失敗,從CacheView中獲取,也是通過(guò)pos獲取holder緩存
    ——>獲取失敗,從自定義緩存中獲取緩存——>獲取失敗,從mRecyclerPool中獲取
    ——>獲取失敗,重新創(chuàng)建viewholder——createViewHolder并bindview。

3)了解了緩存結(jié)構(gòu)和緩存流程,我們?cè)賮?lái)看看具體的問(wèn)題
滑動(dòng)10個(gè),再滑回去,會(huì)有幾個(gè)執(zhí)行onBindView?

  • 由之前的緩存結(jié)構(gòu)可知,需要重新執(zhí)行onBindView的只有一種緩存區(qū),就是緩存池mRecyclerPool。

所以我們假設(shè)從加載RecyclView開(kāi)始盤(pán)的話(頁(yè)面假設(shè)可以容納7條數(shù)據(jù)):

  • 首先,7條數(shù)據(jù)會(huì)依次調(diào)用onCreateViewHolderonBindViewHolder。
  • 往下滑一條(position=7),那么會(huì)把position=0的數(shù)據(jù)放到mCacheViews中。此時(shí)mCacheViews緩存區(qū)數(shù)量為1,mRecyclerPool數(shù)量為0。然后新出現(xiàn)的position=7的數(shù)據(jù)通過(guò)postion在mCacheViews中找不到對(duì)應(yīng)的ViewHolder,通過(guò)itemtype也在mRecyclerPool中找不到對(duì)應(yīng)的數(shù)據(jù),所以會(huì)調(diào)用onCreateViewHolderonBindViewHolder方法。
  • 再往下滑一條數(shù)據(jù)(position=8),如上。
  • 再往下滑一條數(shù)據(jù)(position=9),position=2的數(shù)據(jù)會(huì)放到mCacheViews中,但是由于mCacheViews緩存區(qū)默認(rèn)容量為2,所以position=0的數(shù)據(jù)會(huì)被清空數(shù)據(jù)然后放到mRecyclerPool緩存池中。而新出現(xiàn)的position=9數(shù)據(jù)由于在mRecyclerPool中還是找不到相應(yīng)type的ViewHolder,所以還是會(huì)走onCreateViewHolderonBindViewHolder方法。所以此時(shí)mCacheViews緩存區(qū)數(shù)量為2,mRecyclerPool數(shù)量為1。
  • 再往下滑一條數(shù)據(jù)(position=10),這時(shí)候由于可以在mRecyclerPool中找到相同viewtype的ViewHolder了。所以就直接復(fù)用了,并調(diào)用onBindViewHolder方法綁定數(shù)據(jù)。
  • 后面依次類推,剛消失的兩條數(shù)據(jù)會(huì)被放到mCacheViews中,再出現(xiàn)的時(shí)候是不會(huì)調(diào)用onBindViewHolder方法,而復(fù)用的第三條數(shù)據(jù)是從mRecyclerPool中取得,就會(huì)調(diào)用onBindViewHolder方法了。

4)所以這個(gè)問(wèn)題就得出結(jié)論了(假設(shè)mCacheViews容量為默認(rèn)值2):

  • 如果一開(kāi)始滑動(dòng)的是新數(shù)據(jù),那么滑動(dòng)10個(gè),就會(huì)走10個(gè)bindview方法。然后滑回去,會(huì)走10-2個(gè)bindview方法。一共18次調(diào)用。

  • 如果一開(kāi)始滑動(dòng)的是老數(shù)據(jù),那么滑動(dòng)10-2個(gè),就會(huì)走8個(gè)bindview方法。然后滑回去,會(huì)走10-2個(gè)bindview方法。一共16次調(diào)用。

但是但是,實(shí)際情況又有點(diǎn)不一樣。因?yàn)?code>Recyclerview在v25版本引入了一個(gè)新的機(jī)制,預(yù)取機(jī)制。

預(yù)取機(jī)制,就是在滑動(dòng)過(guò)程中,會(huì)把將要展示的一個(gè)元素提前緩存到mCachedViews中,所以滑動(dòng)10個(gè)元素的時(shí)候,第11個(gè)元素也會(huì)被創(chuàng)建,也就多走了一次bindview方法。但是滑回去的時(shí)候不影響,因?yàn)榫退闾崆叭×艘粋€(gè)緩存數(shù)據(jù),只是把bindview方法提前了,并不影響總的綁定item數(shù)量。

所以滑動(dòng)的是新數(shù)據(jù)的情況下就會(huì)多一次調(diào)用bindview方法。

5)總結(jié),問(wèn)題怎么答呢?

  • 四級(jí)緩存和流程說(shuō)一下。
  • 滑動(dòng)10個(gè),再滑回去,bindview可以是19次調(diào)用,可以是16次調(diào)用。
  • 緩存的其實(shí)就是緩存item的view,在Recyclerview中就是viewholder
  • cachedView就是mCacheViews緩存區(qū)中的view,是不需要重新綁定數(shù)據(jù)的。

如何實(shí)現(xiàn)RecyclerView的局部更新,用過(guò)payload嗎,notifyItemChange方法中的參數(shù)?

關(guān)于RecyclerView的數(shù)據(jù)更新,主要有以下幾個(gè)方法:

  • notifyDataSetChanged(),刷新全部可見(jiàn)的item。
    *notifyItemChanged(int),刷新指定item。
  • notifyItemRangeChanged(int,int),從指定位置開(kāi)始刷新指定個(gè)item。
  • notifyItemInserted(int)、notifyItemMoved(int)、notifyItemRemoved(int)。插入、移動(dòng)一個(gè)并自動(dòng)刷新。
  • notifyItemChanged(int, Object),局部刷新。

可以看到,關(guān)于view的局部刷新就是notifyItemChanged(int, Object)方法,下面具體說(shuō)說(shuō):

notifyItemChange有兩個(gè)構(gòu)造方法:

  • notifyItemChanged(int position, @Nullable Object payload)
  • notifyItemChanged(int position)

其中payload參數(shù)可以認(rèn)為是你要刷新的一個(gè)標(biāo)示,比如我有時(shí)候只想刷新itemView中的textview,有時(shí)候只想刷新imageview?又或者我只想某一個(gè)view的文字顏色進(jìn)行高亮設(shè)置?那么我就可以通過(guò)payload參數(shù)來(lái)標(biāo)示這個(gè)特殊的需求了。

具體怎么做呢?比如我調(diào)用了notifyItemChanged(14,"changeColor"),那么在onBindViewHolder回調(diào)方法中做下判斷即可:

    @Override
    public void onBindViewHolder(ViewHolderholder, int position, List<Object> payloads) {
        if (payloads.isEmpty()) {
            // payloads為空,說(shuō)明是更新整個(gè)ViewHolder
            onBindViewHolder(holder, position);
        } else {
            // payloads不為空,這只更新需要更新的View即可。
            String payload = payloads.get(0).toString();
            if ("changeColor".equals(payload)) {
                holder.textView.setTextColor("");
            }
        }
    }

RecyclerView嵌套R(shí)ecyclerView滑動(dòng)沖突,NestScrollView嵌套R(shí)ecyclerView。

1)RecyclerView嵌套RecyclerView的情況下,如果兩者都要上下滑動(dòng),那么就會(huì)引起滑動(dòng)沖突。默認(rèn)情況下外層的RecyclerView可滑,內(nèi)層不可滑。

之前說(shuō)過(guò)解決滑動(dòng)沖突的辦法有兩種:內(nèi)部攔截法和外部攔截法。
這里我提供一種內(nèi)部攔截法,還有一些其他的辦法大家可以自己思考下。

   holder.recyclerView.setOnTouchListener { v, event ->
            when(event.action){
                //當(dāng)按下操作的時(shí)候,就通知父view不要攔截,拿起操作就設(shè)置可以攔截,正常走父view的滑動(dòng)。
                MotionEvent.ACTION_DOWN,MotionEvent.ACTION_MOVE -> v.parent.requestDisallowInterceptTouchEvent(true)
                MotionEvent.ACTION_UP -> v.parent.requestDisallowInterceptTouchEvent(false)
            }
            false}

2)關(guān)于ScrclerView的滑動(dòng)沖突還是同樣的解決辦法,就是進(jìn)行事件攔截。
還有一個(gè)辦法就是用Nestedscrollview代替ScrollView,Nestedscrollview是官方為了解決滑動(dòng)沖突問(wèn)題而設(shè)計(jì)的新的View。它的定義就是支持嵌套滑動(dòng)的ScrollView。

所以直接替換成Nestedscrollview就能保證兩者都能正?;瑒?dòng)了。但是要注意設(shè)置RecyclerView.setNestedScrollingEnabled(false)這個(gè)方法,用來(lái)取消RecyclerView本身的滑動(dòng)效果。

這是因?yàn)镽ecyclerView默認(rèn)是setNestedScrollingEnabled(true),這個(gè)方法的含義是支持嵌套滾動(dòng)的。也就是說(shuō)當(dāng)它嵌套在NestedScrollView中時(shí),默認(rèn)會(huì)隨著NestedScrollView滾動(dòng)而滾動(dòng),放棄了自己的滾動(dòng)。所以給我們的感覺(jué)就是滯留、卡頓。所以我們將它設(shè)置為false就解決了卡頓問(wèn)題,讓他正常的滑動(dòng),不受外部影響。

說(shuō)說(shuō)RecyclerView性能優(yōu)化。

  • bindViewHolder方法是在UI線程進(jìn)行的,此方法不能耗時(shí)操作,不然將會(huì)影響滑動(dòng)流暢性。比如進(jìn)行日期的格式化。
  • 對(duì)于新增或刪除的時(shí)候,可以使用diffutil進(jìn)行局部刷新,少用全局刷新
  • 對(duì)于itemVIew進(jìn)行布局優(yōu)化,比如少嵌套等。
  • 25.1.0 (>=21)及以上使用Prefetch 功能,也就是預(yù)取功能,嵌套時(shí)且使用的是LinearLayoutManager,子RecyclerView可通過(guò)setInitialPrefatchItemCount設(shè)置預(yù)取個(gè)數(shù)
  • 加大RecyclerView緩存,比如cacheview大小默認(rèn)為2,可以設(shè)置大點(diǎn),用空間來(lái)?yè)Q取時(shí)間,提高流暢度
  • 如果高度固定,可以設(shè)置setHasFixedSize(true)來(lái)避免requestLayout浪費(fèi)資源,否則每次更新數(shù)據(jù)都會(huì)重新測(cè)量高度。
void onItemsInsertedOrRemoved() {
   if (hasFixedSize) layoutChildren();
   else requestLayout();
}
  • 如果多個(gè)RecycledView 的 Adapter 是一樣的,比如嵌套的 RecyclerView 中存在一樣的 Adapter,可以通過(guò)設(shè)置 RecyclerView.setRecycledViewPool(pool);來(lái)共用一個(gè) RecycledViewPool。這樣就減少了創(chuàng)建VIewholder的開(kāi)銷。
  • 在RecyclerView的元素比較高,一屏只能顯示一個(gè)元素的時(shí)候,第一次滑動(dòng)到第二個(gè)元素會(huì)卡頓。這種情況就可以通過(guò)設(shè)置額外的緩存空間,重寫(xiě)getExtraLayoutSpace方法即可。
new LinearLayoutManager(this) {
    @Override
    protected int getExtraLayoutSpace(RecyclerView.State state) {
        return size;
    }
};
  • 設(shè)置RecyclerView.addOnScrollListener();來(lái)在滑動(dòng)過(guò)程中停止加載的操作。
  • 減少對(duì)象的創(chuàng)建,比如設(shè)置監(jiān)聽(tīng)事件,可以全局創(chuàng)建一個(gè),所有view公用一個(gè)listener,并且放到CreateView里面去創(chuàng)建監(jiān)聽(tīng),因?yàn)镃reateView調(diào)用要少于bindview。這樣就減少了對(duì)象創(chuàng)建所造成的消耗
  • notifyDataSetChange時(shí),適配器不知道整個(gè)數(shù)據(jù)集中的那些內(nèi)容以及存在,再重新匹配ViewHolder時(shí)會(huì)花生閃爍。設(shè)置adapter.setHasStableIds(true),并重寫(xiě)getItemId()來(lái)給每個(gè)Item一個(gè)唯一的ID,也就是唯一標(biāo)識(shí),就使itemview的焦點(diǎn)固定,解決了閃爍問(wèn)題。

拜拜

今天聊了不少,關(guān)于RecyclerView重要的知識(shí)點(diǎn)應(yīng)該都涉及到了,其中bindview的問(wèn)題下次有機(jī)會(huì)我會(huì)再配合圖片日志詳細(xì)的說(shuō)一下。

有一起學(xué)習(xí)的小伙伴可以關(guān)注下我的公眾號(hào)——碼上積木????
每日三問(wèn)知識(shí)點(diǎn)/面試題,積少成多。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 為什么提出來(lái)RecycleView? RecyclerView提供了一種插拔式的體驗(yàn),高度解耦,使用靈活。Recy...
    9283856ddec1閱讀 450評(píng)論 0 0
  • 前言 嗨,大家好,好久不見(jiàn)。一個(gè)月沒(méi)寫(xiě)過(guò)文章了,這里跟大家侃侃這中間發(fā)生了什么。 一個(gè)月前呢,想準(zhǔn)備面試,就網(wǎng)上隨...
    積木zz閱讀 6,504評(píng)論 1 36
  • 目錄介紹 25.0.0.0 請(qǐng)說(shuō)一下RecyclerView?adapter的作用是什么,幾個(gè)方法是做什么用的?如...
    楊充211閱讀 1,112評(píng)論 1 10
  • 【Android 控件 RecyclerView】 概述 RecyclerView是什么 從Android 5.0...
    Rtia閱讀 308,466評(píng)論 27 440
  • 久違的晴天,家長(zhǎng)會(huì)。 家長(zhǎng)大會(huì)開(kāi)好到教室時(shí),離放學(xué)已經(jīng)沒(méi)多少時(shí)間了。班主任說(shuō)已經(jīng)安排了三個(gè)家長(zhǎng)分享經(jīng)驗(yàn)。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,861評(píng)論 16 22

友情鏈接更多精彩內(nèi)容