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

「今天我們重點來講解一下ViewCacheExtension緩存」
public abstract static class ViewCacheExtension {
public abstract View getViewForPositionAndType(
Recycler recycler,
int position,
int type
);
}
ViewCacheExtension是RecyclerView框架預留給開發(fā)者實現(xiàn)自己的緩存邏輯的一個接口。很詭異的是,就算是到2021年的秋天,無論你怎么搜索,還是很難找到正確使用ViewCacheExtension的方法。網(wǎng)上的教程,對它的定性都很一致,由于ViewCacheExtension只提供了getView而沒有提供putView方法,所以它的用處不大。「當然這是錯誤的,本文就是為ViewCacheExtension翻案的?!?/strong> 當我們窮盡所有方法,把RecyclerView調(diào)優(yōu)方案都用盡了的時候,用好ViewCacheExtension就成了將RecyclerView性能優(yōu)化到極致的最后一公里。
曾經(jīng)我也是Too young too simple,說ViewCacheExtension沒什么軟用。下圖引用自我寫的聊聊RecyclerView緩存機制
2. ViewCacheExtension能為性能優(yōu)化做什么?
"減少ItemView的嵌套層級,讓布局盡量輕量級"或者減少ItemView的inflate時長會是RecyclerView性能優(yōu)化的眾多Tips中的其二。這樣的方案當然沒問題。但是現(xiàn)實有可能是,ItemView本身就是很復雜,將它的布局優(yōu)化之后inflate還是很耗時 或者ItemView是前輩寫的,太復雜了,后繼的開發(fā)者無能為力或者不愿意去修改它。 這種情況下如何進一步優(yōu)化到極致。當然你可能會說,我用ConstraintLayout將布局優(yōu)化到極致,我能力強而且能吃苦耐勞,前輩寫的復雜且低效的布局我有信心有能力優(yōu)化好。退一步講,這些你都做的很好了。RecyclerView剛初始化的時候ItemView inflate終歸要耗時,而且是會阻塞線程。假設有個10個ItemView,每個耗時20ms,那也會阻塞主線程200ms,有沒有辦法優(yōu)化呢?
?答案當然是有。用ViewCacheExtension來優(yōu)化。用它來優(yōu)化RecyclerView初始化時創(chuàng)建View對主線程阻塞的時長。
?
3. 從一個案例說起
首先模擬復雜View的場景。TextView的構造方法中休眠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的界面很簡單,就是幾個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>
程序運行結果如下:
我們通過Systrace來看下RecyclerView性能表現(xiàn)
通過上圖我們可以看到。初始化HeavyTextView總共花費了639ms。我們知道Android每幀的耗時超過16ms就要掉幀了。所以相對來說比較卡頓。實際運行程序,也會發(fā)現(xiàn)跳轉(zhuǎn)到該Activity明顯不流暢。
對比下優(yōu)化后的效果。前提是不修改HeavyTextView,仍然休眠100ms
對比RV OnLayout事件,優(yōu)化后的效果只需要76ms。將近10倍的優(yōu)化空間。實際效果是,跳轉(zhuǎn)Activity很順滑很流暢。
4. 優(yōu)化方案
程序UI模型圖如下,從AActivity跳轉(zhuǎn)到BActivity,它有一個RecyclerView列表。
AActivity代碼如下:
圖片版本代碼:
Kotlin版本代碼 方便復制
class AActivity : AppCompatActivity() {
companion object {
//靜態(tài)變量,ArrayList保存開發(fā)者緩存View
var sCustomViewCaches: ArrayList<View> = arrayListOf()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//當AActivity MessageQueue有空閑的時候,創(chuàng)建10個HeavyText布局ItemView
Looper.myQueue().addIdleHandler {
thread {
repeat(10) {
val linearLayout = LinearLayout(this@AActivity).apply {
orientation = LinearLayout.VERTICAL
}
//將itemView add到linearLayout上,后有remove掉,為了正確的將item布局中padding顯示出來
val itemView = LayoutInflater.from(this@AActivity)
.inflate(R.layout.custom_cache_view_item, linearLayout)
linearLayout.removeView(itemView)
//背景設置成紅色為了更好的測試是否用到了正確緩存中的View
itemView.setBackgroundColor(Color.RED)
itemView.layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
// 反射設置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設置成0。具體業(yè)務具體實現(xiàn)。主要是為了復用
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實現(xiàn)如下
圖片版本代碼:
Kotlin版本代碼 方便復制
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實例,實際業(yè)務可以寫的更優(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設置RecyclerView.LayoutParems。

ViewHolder不能為空。解決方案:反射設置ViewHolder。

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

技術交流,歡迎加我微信:ezglumes ,拉你入技術交流群。
推薦閱讀:
覺得不錯,點個在看唄~

