各位戰士,歡迎來到第十九天的戰場。如果說 UI 流暢度戰爭有一處「凡爾登絞肉機」般的核心戰場,那無疑就是 RecyclerView
。幾乎每個應用程式的核心介面都由列表構成,而每一次滾動,都是對我們性能優化技巧的嚴峻考驗。
每個新兵都知道 RecyclerView
優化的第一條軍規:使用 ViewHolder 模式。這當然是基礎中的基礎,是我們必須守住的底線。但是,要打贏這場戰爭,光靠這條底線是遠遠不夠的。當列表變得複雜,內容頻繁更新時,僅僅使用 ViewHolder 就像只拿著步槍去對抗坦克。
今天,我們的任務就是為你的軍火庫添置三款強大的戰術武器,它們將幫助你在最嚴苛的滾動戰役中取得勝利。
setHasFixedSize(true)
—— 預判敵情,避免內耗這是什麼?
這是一個你應該在 RecyclerView
上設定的、極其簡單卻又極其重要的布林值。setHasFixedSize(true)
是在告訴 RecyclerView
一個明確的資訊:「我的整體大小(寬度和高度)不會因為列表項目的增減而改變。」
對於絕大多數佔滿螢幕的 RecyclerView
來說,這個條件都是成立的。
為何重要?
預設情況下,每當你呼叫 adapter.notify...()
系列方法更新資料時,RecyclerView
都會假設它的邊界可能會因為內容變化而改變,因此它會觸發一次 requestLayout()
,重新進行完整的「測量 (Measure)」和「佈局 (Layout)」流程。這是一個非常昂貴的操作,尤其是在資料頻繁變動時。
當你設定了 setHasFixedSize(true)
,就等於給 RecyclerView
吃了一顆定心丸。它知道了自己的尺寸是固定的,所以在收到資料變更通知時,它可以跳過重新測量佈局的步驟,只專注於重繪需要更新的 Item。
如何使用?
在初始化 RecyclerView
後,加上這一行即可。
val recyclerView = findViewById<RecyclerView>(R.id.my_recycler_view)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = myAdapter
// 就是這麼簡單!
recyclerView.setHasFixedSize(true)
RecyclerView
的核心是 View 的回收與複用。預設情況下,每一個 RecyclerView
實例都有一個自己私有的「回收池」(RecycledViewPool),用來存放那些滾出螢幕的 ViewHolder。
想像一個常見的 UI:使用 ViewPager2
實現多個分頁,每個分頁裡都有一個 RecyclerView
;或者在一個垂直的 RecyclerView
裡,巢狀了多個水平的 RecyclerView
。在這種情況下,每個 RecyclerView
都各自為政,維護自己的回收池。當你從第一頁滑到第二頁時,第一頁回收的 ViewHolder 只能靜靜地躺在自己的池子裡,而第二頁的 RecyclerView
卻可能因為池子是空的,而被迫不斷建立新的 View,造成卡頓。
建立一個共享的 RecycledViewPool
,並把它設定給所有需要協同作戰的RecyclerView
。
// 建立一個共享的回收池
val sharedViewPool = RecyclerView.RecycledViewPool()
// 假設 viewPager2 中有多個 Fragment,每個 Fragment 裡的 RecyclerView 都使用這個共享池
// Fragment A
recyclerViewA.setRecycledViewPool(sharedViewPool)
// Fragment B
recyclerViewB.setRecycledViewPool(sharedViewPool)
// 對於巢狀 RecyclerView 同樣適用
// verticalRecyclerViewAdapter onBindViewHolder...
// horizontalRecyclerView.setRecycledViewPool(sharedViewPool)
如此一來,不同列表之間就可以互相支援。從 A 列表滾出的 View,可以被 B 列表無縫地取用(只要它們的 itemViewType
相同),大大提高了 ViewHolder 的複用效率,減少了 onCreateViewHolder 的呼叫次數。
當列表資料需要更新時,你最先想到的可能是 adapter.notifyDataSetChanged()
。這是一個毀滅性的命令,相當於對整個戰場進行「地毯式轟炸」。它告訴 RecyclerView
:「所有東西都變了,放棄所有快取,全部重新佈局和重繪!」 這不僅性能極低,還會失去所有優雅的 Item 動畫。
使用 DiffUtil
進行「精準打擊」。DiffUtil
是一個工具類,它能比較新舊兩個資料列表的差異,並計算出最小的更新操作集(哪些是新增、哪些是刪除、哪些是移動、哪些是內容改變)。
而比 DiffUtil
更進階的武器,是官方推薦的 ListAdapter
。ListAdapter
是一個特殊的 Adapter,它內建了 DiffUtil
的功能,並在背景執行緒為你處理好了一切。
DiffUtil.ItemCallback
。class MyItemDiffCallback : DiffUtil.ItemCallback<MyItem>() {
// 判斷兩個 Item 是否是同一個(通常比較 ID)
override fun areItemsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
return oldItem.id == newItem.id
}
// 判斷同一個 Item 的內容是否發生了變化
override fun areContentsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
return oldItem == newItem // 如果 MyItem 是 data class,可以直接比較
}
}
ListAdapter
。class MyAdapter : ListAdapter<MyItem, MyViewHolder>(MyItemDiffCallback()) {
// ... 正常實現 onCreateViewHolder 和 onBindViewHolder ...
}
notifyDataSetChanged()
,改用 submitList()
。// 在你的 Activity 或 ViewModel 中
val newList = // ... 從網路或資料庫獲取了新資料 ...
myAdapter.submitList(newList)
就這樣!ListAdapter
會自動在背景執行緒用 DiffUtil
比較新舊列表的差異,然後把計算出的精準更新操作(如 notifyItemInserted
, notifyItemChanged
)切換到主執行緒來執行。你將同時獲得極致的性能和免費的 Item 動畫。
今天,我們為 RecyclerView
這場核心戰役裝備了三款超越 ViewHolder 模式的進階武器:
setHasFixedSize(true)
: 透過預判佈局不變來避免不必要的系統內耗。
RecycledViewPool
: 透過共享回收池來提高多列表場景下的 ViewHolder 複用率。
ListAdapter
與 DiffUtil
: 透過精準的資料比對與更新,實現最高效、最流暢的列表刷新。
掌握了這些技巧,你就擁有了解決絕大多數 RecyclerView
卡頓問題的能力。
傳統 View 體系的戰役即將告一段落。明天,戰爭將進入一個全新的維度:Jetpack Compose。我們將探討在這個聲明式 UI 的世界裡,性能戰爭的規則又發生了怎樣的改變,以及如何應對最核心的挑戰——Recomposition (重組)。
我們明天見!