嗨,大家好红淡,最近去淘了一些關(guān)于RecyclerView的面試真題外莲,大家一起看看吧猪半,這次的問題如果都弄懂了,下次面試再遇到RecyclerView應(yīng)該就沒啥可擔(dān)心的了。
- 講一下
RecyclerView
的緩存機(jī)制,滑動10個磨确,再滑回去沽甥,會有幾個執(zhí)行onBindView
。緩存的是什么俐填?cachedView
會執(zhí)行onBindView嗎? -
RecyclerView
預(yù)取機(jī)制 - 如何實(shí)現(xiàn)
RecyclerView
的局部更新安接,用過payload
嗎,notifyItemChange方法中的參數(shù)? -
RecyclerView
嵌套RecyclerView
滑動沖突英融,NestScrollView嵌套RecyclerView。 - 說說
RecyclerView
性能優(yōu)化歇式。
講一下RecyclerView的緩存機(jī)制,滑動10個驶悟,再滑回去,會有幾個執(zhí)行onBindView材失。緩存的是什么痕鳍?cachedView會執(zhí)行onBindView嗎?
RecyclerView預(yù)取機(jī)制
這兩個問題都是關(guān)于緩存的,我就一起說了龙巨。
1)首先說下RecyclerView的緩存結(jié)構(gòu):
Recyclerview有四級緩存笼呆,分別是mAttachedScrap(屏幕內(nèi)),mCacheViews(屏幕外)旨别,mViewCacheExtension(自定義緩存)诗赌,mRecyclerPool(緩存池)
-
mAttachedScrap(屏幕內(nèi))
,用于屏幕內(nèi)itemview快速重用秸弛,不需要重新createView和bindView -
mCacheViews(屏幕外)
铭若,保存最近移出屏幕的ViewHolder,包含數(shù)據(jù)和position信息递览,復(fù)用時必須是相同位置的ViewHolder才能復(fù)用叼屠,應(yīng)用場景在那些需要來回滑動的列表中,當(dāng)往回滑動時绞铃,能直接復(fù)用ViewHolder數(shù)據(jù)镜雨,不需要重新bindView。 -
mViewCacheExtension(自定義緩存)
儿捧,不直接使用荚坞,需要用戶自定義實(shí)現(xiàn),默認(rèn)不實(shí)現(xiàn)纯命。 -
mRecyclerPool(緩存池)
西剥,當(dāng)cacheView滿了后或者adapter被更換,將cacheView中移出的ViewHolder放到Pool中亿汞,放之前會把ViewHolder數(shù)據(jù)清除掉瞭空,所以復(fù)用時需要重新bindView。
2)四級緩存按照順序需要依次讀取。所以完整緩存流程是:
- 保存緩存流程:
- 插入或是刪除
itemView
時咆畏,先把屏幕內(nèi)的ViewHolder保存至AttachedScrap
中 - 滑動屏幕的時候南捂,先消失的itemview會保存到
CacheView
,CacheView大小默認(rèn)是2旧找,超過數(shù)量的話按照先入先出原則溺健,移出頭部的itemview保存到RecyclerPool緩存池
(如果有自定義緩存就會保存到自定義緩存里),RecyclerPool緩存池會按照itemview的itemtype
進(jìn)行保存钮蛛,每個itemType緩存?zhèn)€數(shù)為5個鞭缭,超過就會被回收。
- 獲取緩存流程:
- AttachedScrap中獲取魏颓,通過pos匹配holder——>獲取失敗岭辣,從
CacheView
中獲取,也是通過pos獲取holder緩存
——>獲取失敗甸饱,從自定義緩存
中獲取緩存——>獲取失敗沦童,從mRecyclerPool
中獲取
——>獲取失敗,重新創(chuàng)建viewholder
——createViewHolder并bindview叹话。
3)了解了緩存結(jié)構(gòu)和緩存流程偷遗,我們再來看看具體的問題
滑動10個,再滑回去驼壶,會有幾個執(zhí)行onBindView氏豌?
- 由之前的緩存結(jié)構(gòu)可知,需要重新執(zhí)行
onBindView
的只有一種緩存區(qū)辅柴,就是緩存池mRecyclerPool
箩溃。
所以我們假設(shè)從加載RecyclView
開始盤的話(頁面假設(shè)可以容納7條數(shù)據(jù)):
- 首先,7條數(shù)據(jù)會依次調(diào)用
onCreateViewHolder
和onBindViewHolder
碌嘀。 - 往下滑一條(position=7)涣旨,那么會把position=0的數(shù)據(jù)放到
mCacheViews
中。此時mCacheViews
緩存區(qū)數(shù)量為1股冗,mRecyclerPool
數(shù)量為0霹陡。然后新出現(xiàn)的position=7的數(shù)據(jù)通過postion在mCacheViews
中找不到對應(yīng)的ViewHolder
,通過itemtype
也在mRecyclerPool
中找不到對應(yīng)的數(shù)據(jù)止状,所以會調(diào)用onCreateViewHolder
和onBindViewHolder
方法烹棉。 - 再往下滑一條數(shù)據(jù)(position=8),如上怯疤。
- 再往下滑一條數(shù)據(jù)(position=9)浆洗,position=2的數(shù)據(jù)會放到
mCacheViews
中,但是由于mCacheViews
緩存區(qū)默認(rèn)容量為2集峦,所以position=0的數(shù)據(jù)會被清空數(shù)據(jù)然后放到mRecyclerPool
緩存池中伏社。而新出現(xiàn)的position=9數(shù)據(jù)由于在mRecyclerPool
中還是找不到相應(yīng)type的ViewHolder抠刺,所以還是會走onCreateViewHolder
和onBindViewHolder
方法。所以此時mCacheViews
緩存區(qū)數(shù)量為2摘昌,mRecyclerPool
數(shù)量為1速妖。 - 再往下滑一條數(shù)據(jù)(position=10),這時候由于可以在
mRecyclerPool
中找到相同viewtype的ViewHolder了聪黎。所以就直接復(fù)用了罕容,并調(diào)用onBindViewHolder
方法綁定數(shù)據(jù)。 - 后面依次類推稿饰,剛消失的兩條數(shù)據(jù)會被放到
mCacheViews
中锦秒,再出現(xiàn)的時候是不會調(diào)用onBindViewHolder方法,而復(fù)用的第三條數(shù)據(jù)是從mRecyclerPool
中取得湘纵,就會調(diào)用onBindViewHolder
方法了脂崔。
4)所以這個問題就得出結(jié)論了(假設(shè)mCacheViews
容量為默認(rèn)值2):
如果一開始滑動的是新數(shù)據(jù),那么滑動10個梧喷,就會走10個
bindview
方法。然后滑回去脖咐,會走10-2個bindview
方法铺敌。一共18次調(diào)用。如果一開始滑動的是老數(shù)據(jù)屁擅,那么滑動10-2個偿凭,就會走8個
bindview
方法。然后滑回去派歌,會走10-2個bindview
方法弯囊。一共16次調(diào)用。
但是但是胶果,實(shí)際情況又有點(diǎn)不一樣匾嘱。因?yàn)?code>Recyclerview在v25版本引入了一個新的機(jī)制,預(yù)取機(jī)制
早抠。
預(yù)取機(jī)制
霎烙,就是在滑動過程中,會把將要展示的一個元素提前緩存到mCachedViews
中蕊连,所以滑動10個元素的時候悬垃,第11個元素也會被創(chuàng)建,也就多走了一次bindview
方法甘苍。但是滑回去的時候不影響尝蠕,因?yàn)榫退闾崆叭×艘粋€緩存數(shù)據(jù),只是把bindview
方法提前了载庭,并不影響總的綁定item數(shù)量看彼。
所以滑動的是新數(shù)據(jù)的情況下就會多一次調(diào)用bindview
方法廊佩。
5)總結(jié),問題怎么答呢闲昭?
- 四級緩存和流程說一下罐寨。
- 滑動10個,再滑回去序矩,
bindview
可以是19次調(diào)用鸯绿,可以是16次調(diào)用。 - 緩存的其實(shí)就是緩存item的view簸淀,在Recyclerview中就是
viewholder
瓶蝴。 -
cachedView
就是mCacheViews
緩存區(qū)中的view,是不需要重新綁定數(shù)據(jù)的租幕。
如何實(shí)現(xiàn)RecyclerView的局部更新舷手,用過payload嗎,notifyItemChange方法中的參數(shù)?
關(guān)于RecyclerView的數(shù)據(jù)更新劲绪,主要有以下幾個方法:
-
notifyDataSetChanged()
男窟,刷新全部可見的item。
*notifyItemChanged(int)
贾富,刷新指定item歉眷。 -
notifyItemRangeChanged(int,int)
,從指定位置開始刷新指定個item颤枪。 -
notifyItemInserted(int)汗捡、notifyItemMoved(int)、notifyItemRemoved(int)
畏纲。插入扇住、移動一個并自動刷新。 -
notifyItemChanged(int, Object)
盗胀,局部刷新艘蹋。
可以看到,關(guān)于view的局部刷新就是notifyItemChanged(int, Object)方法读整,下面具體說說:
notifyItemChange
有兩個構(gòu)造方法:
- notifyItemChanged(int position, @Nullable Object payload)
- notifyItemChanged(int position)
其中payload
參數(shù)可以認(rèn)為是你要刷新的一個標(biāo)示簿训,比如我有時候只想刷新itemView
中的textview
,有時候只想刷新imageview
?又或者我只想某一個view的文字顏色進(jìn)行高亮設(shè)置米间?那么我就可以通過payload
參數(shù)來標(biāo)示這個特殊的需求了强品。
具體怎么做呢?比如我調(diào)用了notifyItemChanged(14,"changeColor")
,那么在onBindViewHolder
回調(diào)方法中做下判斷即可:
@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
的情況下雕薪,如果兩者都要上下滑動,那么就會引起滑動沖突晓淀。默認(rèn)情況下外層的RecyclerView可滑所袁,內(nèi)層不可滑。
之前說過解決滑動沖突的辦法有兩種:內(nèi)部攔截法和外部攔截法凶掰。
這里我提供一種內(nèi)部攔截法燥爷,還有一些其他的辦法大家可以自己思考下。
holder.recyclerView.setOnTouchListener { v, event ->
when(event.action){
//當(dāng)按下操作的時候懦窘,就通知父view不要攔截前翎,拿起操作就設(shè)置可以攔截,正常走父view的滑動畅涂。
MotionEvent.ACTION_DOWN,MotionEvent.ACTION_MOVE -> v.parent.requestDisallowInterceptTouchEvent(true)
MotionEvent.ACTION_UP -> v.parent.requestDisallowInterceptTouchEvent(false)
}
false}
2)關(guān)于ScrclerView
的滑動沖突還是同樣的解決辦法港华,就是進(jìn)行事件攔截。
還有一個辦法就是用Nestedscrollview
代替ScrollView
午衰,Nestedscrollview
是官方為了解決滑動沖突問題而設(shè)計的新的View立宜。它的定義就是支持嵌套滑動的ScrollView。
所以直接替換成Nestedscrollview
就能保證兩者都能正畴叮滑動了赘理。但是要注意設(shè)置RecyclerView.setNestedScrollingEnabled(false)
這個方法,用來取消RecyclerView本身的滑動效果扇单。
這是因?yàn)镽ecyclerView默認(rèn)是setNestedScrollingEnabled(true)
,這個方法的含義是支持嵌套滾動的奠旺。也就是說當(dāng)它嵌套在NestedScrollView
中時,默認(rèn)會隨著NestedScrollView
滾動而滾動,放棄了自己的滾動蜘澜。所以給我們的感覺就是滯留、卡頓响疚。所以我們將它設(shè)置為false就解決了卡頓問題鄙信,讓他正常的滑動,不受外部影響装诡。
說說RecyclerView性能優(yōu)化鸦采。
-
bindViewHolder
方法是在UI線程進(jìn)行的渔伯,此方法不能耗時操作锣吼,不然將會影響滑動流暢性玄叠。比如進(jìn)行日期的格式化读恃。 - 對于新增或刪除的時候狐粱,可以使用
diffutil
進(jìn)行局部刷新肌蜻,少用全局刷新 - 對于
itemVIew
進(jìn)行布局優(yōu)化蒋搜,比如少嵌套等育谬。 - 25.1.0 (>=21)及以上使用
Prefetch
功能膛檀,也就是預(yù)取功能咖刃,嵌套時且使用的是LinearLayoutManager嚎杨,子RecyclerView可通過setInitialPrefatchItemCount設(shè)置預(yù)取個數(shù) - 加大
RecyclerView緩存
,比如cacheview大小默認(rèn)為2箩帚,可以設(shè)置大點(diǎn)膏潮,用空間來換取時間焕参,提高流暢度 - 如果高度固定刻帚,可以設(shè)置
setHasFixedSize(true)
來避免requestLayout浪費(fèi)資源崇众,否則每次更新數(shù)據(jù)都會重新測量高度顷歌。
void onItemsInsertedOrRemoved() {
if (hasFixedSize) layoutChildren();
else requestLayout();
}
- 如果多個
RecycledView
的 Adapter 是一樣的,比如嵌套的 RecyclerView 中存在一樣的 Adapter麻顶,可以通過設(shè)置RecyclerView.setRecycledViewPool(pool);
來共用一個RecycledViewPool
队萤。這樣就減少了創(chuàng)建VIewholder的開銷要尔。 - 在RecyclerView的元素比較高,一屏只能顯示一個元素的時候,第一次滑動到第二個元素會卡頓旁钧。這種情況就可以通過設(shè)置額外的緩存空間,重寫
getExtraLayoutSpace
方法即可寄猩。
new LinearLayoutManager(this) {
@Override
protected int getExtraLayoutSpace(RecyclerView.State state) {
return size;
}
};
- 設(shè)置
RecyclerView.addOnScrollListener();
來在滑動過程中停止加載的操作替废。 - 減少對象的創(chuàng)建,比如設(shè)置監(jiān)聽事件状答,可以全局創(chuàng)建一個惊科,所有view公用一個listener,并且放到
CreateView
里面去創(chuàng)建監(jiān)聽孙咪,因?yàn)镃reateView調(diào)用要少于bindview。這樣就減少了對象創(chuàng)建所造成的消耗 - 用
notifyDataSetChange
時荤堪,適配器不知道整個數(shù)據(jù)集中的那些內(nèi)容以及存在,再重新匹配ViewHolder
時會花生閃爍。設(shè)置adapter.setHasStableIds(true)肮塞,并重寫getItemId()
來給每個Item一個唯一的ID,也就是唯一標(biāo)識拷窜,就使itemview的焦點(diǎn)固定赋荆,解決了閃爍問題。
拜拜
今天聊了不少,關(guān)于RecyclerView重要的知識點(diǎn)應(yīng)該都涉及到了均抽,其中bindview
的問題下次有機(jī)會我會再配合圖片日志詳細(xì)的說一下款熬。
有一起學(xué)習(xí)的小伙伴可以關(guān)注下我的公眾號——碼上積木????
每日三問知識點(diǎn)/面試題惋鹅,積少成多。