RecyclerView性能優(yōu)化的最后一公里
1. 前言
時(shí)至今日相信大部分的Android開(kāi)發(fā)者對(duì)RecyclerView的緩存機(jī)制如數(shù)家珍。相關(guān)教程也是數(shù)不勝數(shù)。如果你想詳細(xì)了解這些不同緩存的作用以及實(shí)現(xiàn)原理??梢詤⒖嘉抑皩戇^(guò)的兩篇文章。聊聊RecyclerView緩存機(jī)制和詳細(xì)聊聊RecyclerView緩存機(jī)制,前者主要是介紹各個(gè)層級(jí)緩存的作用以及它們之間的區(qū)別,后者主要是從源碼的角度講解緩存是怎么實(shí)現(xiàn)的。緩存架構(gòu)圖如下:

「今天我們重點(diǎn)來(lái)講解一下ViewCacheExtension緩存」
public abstract static class ViewCacheExtension {
public abstract View getViewForPositionAndType(
Recycler recycler,
int position,
int type
);
}
ViewCacheExtension是RecyclerView框架預(yù)留給開(kāi)發(fā)者實(shí)現(xiàn)自己的緩存邏輯的一個(gè)接口。很詭異的是,就算是到2021年的秋天,無(wú)論你怎么搜索,還是很難找到正確使用ViewCacheExtension的方法。網(wǎng)上的教程,對(duì)它的定性都很一致,由于ViewCacheExtension只提供了getView而沒(méi)有提供putView方法,所以它的用處不大。「當(dāng)然這是錯(cuò)誤的,本文就是為ViewCacheExtension翻案的?!?/strong> 當(dāng)我們窮盡所有方法,把RecyclerView調(diào)優(yōu)方案都用盡了的時(shí)候,用好ViewCacheExtension就成了將RecyclerView性能優(yōu)化到極致的最后一公里。
曾經(jīng)我也是Too young too simple,說(shuō)ViewCacheExtension沒(méi)什么軟用。下圖引用自我寫的聊聊RecyclerView緩存機(jī)制
2. ViewCacheExtension能為性能優(yōu)化做什么?
"減少ItemView的嵌套層級(jí),讓布局盡量輕量級(jí)"或者減少ItemView的inflate時(shí)長(zhǎng)會(huì)是RecyclerView性能優(yōu)化的眾多Tips中的其二。這樣的方案當(dāng)然沒(méi)問(wèn)題。但是現(xiàn)實(shí)有可能是,ItemView本身就是很復(fù)雜,將它的布局優(yōu)化之后inflate還是很耗時(shí) 或者ItemView是前輩寫的,太復(fù)雜了,后繼的開(kāi)發(fā)者無(wú)能為力或者不愿意去修改它。 這種情況下如何進(jìn)一步優(yōu)化到極致。當(dāng)然你可能會(huì)說(shuō),我用ConstraintLayout將布局優(yōu)化到極致,我能力強(qiáng)而且能吃苦耐勞,前輩寫的復(fù)雜且低效的布局我有信心有能力優(yōu)化好。退一步講,這些你都做的很好了。RecyclerView剛初始化的時(shí)候ItemView inflate終歸要耗時(shí),而且是會(huì)阻塞線程。假設(shè)有個(gè)10個(gè)ItemView,每個(gè)耗時(shí)20ms,那也會(huì)阻塞主線程200ms,有沒(méi)有辦法優(yōu)化呢?
?答案當(dāng)然是有。用ViewCacheExtension來(lái)優(yōu)化。用它來(lái)優(yōu)化RecyclerView初始化時(shí)創(chuàng)建View對(duì)主線程阻塞的時(shí)長(zhǎng)。
?
3. 從一個(gè)案例說(shuō)起
首先模擬復(fù)雜View的場(chǎng)景。TextView的構(gòu)造方法中休眠100ms。
class HeavyTextView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : androidx.appcompat.widget.AppCompatTextView(context, attrs, defStyleAttr) {
init {
println("heavy view init")
Thread.sleep(100L)
}
}
RecyclerView的界面很簡(jiǎn)單,就是幾個(gè)TextView。itemView布局文件代碼如下:
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginTop="5dp"
android:layout_marginRight="5dp"
android:layout_marginBottom="5dp">
<com.peter.viewgrouptutorial.recyclerview.HeavyTextView
android:id="@+id/heavy.text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/white_touch"
android:clickable="true"
android:orientation="horizontal"
android:padding="@dimen/small"
android:textSize="14sp" />
</androidx.cardview.widget.CardView>
程序運(yùn)行結(jié)果如下:
我們通過(guò)Systrace來(lái)看下RecyclerView性能表現(xiàn)
通過(guò)上圖我們可以看到。初始化HeavyTextView總共花費(fèi)了639ms。我們知道Android每幀的耗時(shí)超過(guò)16ms就要掉幀了。所以相對(duì)來(lái)說(shuō)比較卡頓。實(shí)際運(yùn)行程序,也會(huì)發(fā)現(xiàn)跳轉(zhuǎn)到該Activity明顯不流暢。
對(duì)比下優(yōu)化后的效果。前提是不修改HeavyTextView,仍然休眠100ms
對(duì)比RV OnLayout事件,優(yōu)化后的效果只需要76ms。將近10倍的優(yōu)化空間。實(shí)際效果是,跳轉(zhuǎn)Activity很順滑很流暢。
4. 優(yōu)化方案
程序UI模型圖如下,從AActivity跳轉(zhuǎn)到BActivity,它有一個(gè)RecyclerView列表。
AActivity代碼如下:
圖片版本代碼:
Kotlin版本代碼 方便復(fù)制
class AActivity : AppCompatActivity() {
companion object {
//靜態(tài)變量,ArrayList保存開(kāi)發(fā)者緩存View
var sCustomViewCaches: ArrayList<View> = arrayListOf()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//當(dāng)AActivity MessageQueue有空閑的時(shí)候,創(chuàng)建10個(gè)HeavyText布局ItemView
Looper.myQueue().addIdleHandler {
thread {
repeat(10) {
val linearLayout = LinearLayout(this@AActivity).apply {
orientation = LinearLayout.VERTICAL
}
//將itemView add到linearLayout上,后有remove掉,為了正確的將item布局中padding顯示出來(lái)
val itemView = LayoutInflater.from(this@AActivity)
.inflate(R.layout.custom_cache_view_item, linearLayout)
linearLayout.removeView(itemView)
//背景設(shè)置成紅色為了更好的測(cè)試是否用到了正確緩存中的View
itemView.setBackgroundColor(Color.RED)
itemView.layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
// 反射設(shè)置RecyclerView.LayoutParams的mViewHolder屬性
val viewHolderField =
RecyclerView.LayoutParams::class.java.getDeclaredField("mViewHolder")
.apply {
isAccessible = true
}
//等效于Adapter中的onCreateViewHolder方法,創(chuàng)建ViewHolder
val viewHolder = object : RecyclerView.ViewHolder(itemView) {}
//將ViewHolder的mItemViewType設(shè)置成0。具體業(yè)務(wù)具體實(shí)現(xiàn)。主要是為了復(fù)用
with(
RecyclerView.ViewHolder::class.java.getDeclaredField("mItemViewType")
.apply {
isAccessible = true
}) {
set(viewHolder, 0)
}
viewHolderField.set(itemView.layoutParams, viewHolder)
//將ItemView保存到緩存中
sCustomViewCaches.add(itemView)
}
println("custom view cache ok")
}
false
}
}
}
BActivity實(shí)現(xiàn)如下
圖片版本代碼:
Kotlin版本代碼 方便復(fù)制
class BActivity : AppCompatActivity() {
private lateinit var mRecyclerView: RecyclerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recycler_view_custom_cache)
mRecyclerView = findViewById(R.id.recyclerview)
//省略很多RecyclerView的常規(guī)操作比如setAdapter和LayoutManager
mRecyclerView.setViewCacheExtension(object : RecyclerView.ViewCacheExtension() {
override fun getViewForPositionAndType(
recycler: RecyclerView.Recycler,
position: Int,
type: Int
): View? {
//從AActivity的緩存中拿View,Demo實(shí)例,實(shí)際業(yè)務(wù)可以寫的更優(yōu)雅
if (AActivity.sCustomViewCaches.size != 0) {
val view = DashboardActivity.sCustomViewCaches.removeFirst()
println("custom cache view remove $position $view")
if (position == 0) {
println("attention $position $view")
}
return view
}
return null
}
})
}
}
5.遇到的坑
空指針異常。解決方案:為itemView設(shè)置RecyclerView.LayoutParems。

ViewHolder不能為空。解決方案:反射設(shè)置ViewHolder。

布局間距不正確。解決方案:先將itemView add到臨時(shí)viewGroup上,然后remove掉。
緩存復(fù)用不正確。解決方案:反射設(shè)置ViewHolder的itemViewType。
緩存不夠用。原因RecyclerView的layout_height="wrap_content",解決方案:"設(shè)置成match_parent"。與測(cè)量機(jī)制有關(guān)。
「以上坑,本案例全部解決過(guò)了,期待并感謝您的素質(zhì)三連-> 點(diǎn)贊、在看、分享」

技術(shù)交流,歡迎加我微信:ezglumes ,拉你入技術(shù)交流群。
推薦閱讀:
開(kāi)通專輯 | 細(xì)數(shù)那些年寫過(guò)的技術(shù)文章專輯
NDK 學(xué)習(xí)進(jìn)階免費(fèi)視頻來(lái)了
推薦幾個(gè)堪稱教科書(shū)級(jí)別的 Android 音視頻入門項(xiàng)目
覺(jué)得不錯(cuò),點(diǎn)個(gè)在看唄~

