1. 前言
时至今日相信大部分的Android开发者对RecyclerView的缓存机制如数家珍。相关教程也是数不胜数。如果你想详细了解这些不同缓存的作用以及实现原理。可以参考我之前写过的两篇文章。聊聊RecyclerView缓存机制和详细聊聊RecyclerView缓存机制,前者主要是介绍各个层级缓存的作用以及它们之间的区别,后者主要是从源码的角度讲解缓存是怎么实现的。缓存架构图如下:
「今天我们重点来讲解一下ViewCacheExtension缓存」
代码语言:javascript复制public abstract static class ViewCacheExtension {
public abstract View getViewForPositionAndType(
Recycler recycler,
int position,
int type
);
}
ViewCacheExtension是RecyclerView框架预留给开发者实现自己的缓存逻辑的一个接口。很诡异的是,就算是到2021年的秋天,无论你怎么搜索,还是很难找到正确使用ViewCacheExtension的方法。网上的教程,对它的定性都很一致,由于ViewCacheExtension只提供了getView而没有提供putView方法,所以它的用处不大
。「当然这是错误的,本文就是为ViewCacheExtension翻案的。」 当我们穷尽所有方法,把RecyclerView调优方案都用尽了的时候,用好ViewCacheExtension就成了将RecyclerView性能优化到极致的最后一公里。
曾经我也是Too young too simple,说ViewCacheExtension没什么软用。下图引用自我写的聊聊RecyclerView缓存机制
2. ViewCacheExtension能为性能优化做什么?
"减少ItemView的嵌套层级,让布局尽量轻量级"
或者减少ItemView的inflate时长
会是RecyclerView性能优化的众多Tips中的其二。这样的方案当然没问题。但是现实有可能是,ItemView本身就是很复杂,将它的布局优化之后inflate还是很耗时
或者ItemView是前辈写的,太复杂了,后继的开发者无能为力或者不愿意去修改它。
这种情况下如何进一步优化到极致。当然你可能会说,我用ConstraintLayout将布局优化到极致,我能力强而且能吃苦耐劳,前辈写的复杂且低效的布局我有信心有能力优化好。退一步讲,这些你都做的很好了。RecyclerView刚初始化的时候ItemView inflate终归要耗时,而且是会阻塞线程。假设有个10个ItemView,每个耗时20ms,那也会阻塞主线程200ms,有没有办法优化呢?
❝答案当然是有。用ViewCacheExtension来优化。用它来优化RecyclerView初始化时创建View对主线程阻塞的时长。 ❞
3. 从一个案例说起
首先模拟复杂View的场景。TextView的构造方法中休眠100ms。
代码语言:javascript复制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布局文件代码如下:
代码语言:javascript复制<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性能表现
通过上图我们可以看到。初始化HeavyTextView总共花费了639ms。我们知道Android每帧的耗时超过16ms就要掉帧了。所以相对来说比较卡顿。实际运行程序,也会发现跳转到该Activity明显不流畅。
对比下优化后的效果。前提是不修改HeavyTextView,仍然休眠100ms
对比RV OnLayout
事件,优化后的效果只需要76ms。将近10倍的优化空间。实际效果是,跳转Activity很顺滑很流畅。
4. 优化方案
程序UI模型图如下,从AActivity跳转到BActivity,它有一个RecyclerView列表。
AActivity代码如下:
图片版本代码:
Kotlin版本代码
方便复制
class AActivity : AppCompatActivity() {
companion object {
//静态变量,ArrayList保存开发者缓存View
var sCustomViewCaches: ArrayList<View> = arrayListOf()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//当AActivity MessageQueue有空闲的时候,创建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方法,创建ViewHolder
val viewHolder = object : RecyclerView.ViewHolder(itemView) {}
//将ViewHolder的mItemViewType设置成0。具体业务具体实现。主要是为了复用
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实现如下
图片版本代码:
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的常规操作比如setAdapter和LayoutManager
mRecyclerView.setViewCacheExtension(object : RecyclerView.ViewCacheExtension() {
override fun getViewForPositionAndType(
recycler: RecyclerView.Recycler,
position: Int,
type: Int
): View? {
//从AActivity的缓存中拿View,Demo实例,实际业务可以写的更优雅
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"。与测量机制有关。