RecyclerView优化

December 26, 2019

一、数据拉取与处理

1.1 数据处理

大家都知道,数据从网络和本地磁盘加载到内存,为避免主线程阻塞会在io线程上进行。但绑定视图数据时,重量级转换操作也会导致列表滚动卡顿。如果能够预计算,如:字符串替换、拼接、类转换,数据填充到 Adapter 前和加载过程一并在 computation 线程完成转换。

1.2 数据缓存

ViewHolder 共享数据从 Adapter 获取公共变量,避免保存多个副本或要求实时计算。

1.3 局部更新

局部更新可减少刷新所需时间,推荐使用 DiffUtil 计算数据集。

1
2
3
4
fun addItem(reply: Reply) {
    list.add(0, reply)
    notifyItemInserted(0)
}

二、视图优化

2.1 填充次数

RecyclerView 三级缓存目的是复用已填充视图。当缓存数量不足填满屏幕而频繁创建,滑出屏幕后又超过缓存阈值被销毁,多发生在高度很小的视图。这既占用处理器时间片,同时又产生临时对象。

recyclerview_notice

根据经验来看,RecyclerView 默认缓存阈值偏小,不同类型需要缓存最大数量也不同。最好的方式,是根据屏幕和视图尺寸动态计算分类缓存所需数量。

2.2 绘制优化

减少过度绘制同样适用于 RecyclerView 视图布局,提高滑动帧率。此外,部分 item 可能通过自定义 View 绘制视图,因此也需遵守其性能优化规范。

2.3 布局填充时间

LayoutInflater 实例化视图时不仅需要遍历xml节点,而且视图用反射实例化,导致复杂视图耗时较长,列表快速滚动容易卡顿。降低布局复杂度、增加布局缓存数量都能有效缓解。虽然 ConstraintLayout 具备去除布局层次的能力,不过很多开发者发现和 RecyclerView 使用有严重问题。

原生视图组装、AnkoLithoJetPack compose 原理都类似,没有xml遍历和类反射而提升性能。

不过上述方案各自缺点也明显:原生视图编写代码量大;Anko 配合style使用要自定义方法;Litho 由于技术实现令视图灵活性较低;JetPack compose 还处于实验性阶段,功能不够完善。按照现在的发展形势来看,个人推荐 AnkoJetPack compose

原生视图组装:

1
2
3
4
5
6
7
8
9
10
11
12
13
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val params = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
    params.gravity = Gravity.CENTER

    val button = Button(this)
    button.text = "Start"
    button.isAllCaps = false
    button.setOnClickListener { startActivity<MessagesActivity>() }

    addContentView(button, params)
}

Anko 代码示例 phantomVK/MessageKit - MessageHolder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class TextMessageLayout : AnkoComponent<ViewGroup> {
    override fun createView(ui: AnkoContext<ViewGroup>) = with(ui) {
        frameLayout {
            lparams(WRAP_CONTENT, WRAP_CONTENT)

            textView {
                id = R.id.text
                autoLinkMask = Linkify.EMAIL_ADDRESSES or Linkify.WEB_URLS
                gravity = Gravity.CENTER_VERTICAL
                includeFontPadding = false
                minimumHeight = dip(40)
                padding = dip(10)
                textColor = Color.parseColor("#222222")
                setTextIsSelectable(false)
                textSize = 16f //sp
            }
        }
    }
}

Jetpack compose 官方示例 Jetpack Compose Basics

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Preview
@Composable
fun NewsStory() {
    val image = +imageResource(R.drawable.header)
    MaterialTheme {
            Column(
                modifier = Spacing(16.dp)
            ) {
                Container(modifier = Height(180.dp) wraps Expanded) {
                    Clip(shape = RoundedCornerShape(8.dp)) {
                        DrawImage(image)
                    }
                }
                HeightSpacer(16.dp)
                Text("A day in Shark Fin Cove")
                Text("Davenport, California")
                Text("December 2018")
            }
    }
}

关于 Litho 的使用美团有深入研究,参考 基本功 - Litho的使用及原理剖析Litho在美团动态化方案MTFlexbox中的实践

2.4 对象生成

RecyclerView 视图分类较多时,缓存元素总数也会很多,加上复杂视图 ViewHolder 很多数据成员占用很多内存。用测量工具对比列表展示前后内存占用,可以大概确定用量。结构样式相似的视图可用 View.visibility 合并类别。

三、参数配置

RecyclerView 自有参数也能提升渲染性能。

3.1 setHasFixedSize

描述 RecyclerView 是否根据items总宽高重新计算大小。多数情况 RecyclerView 宽高是 MATCH_PARENT,设置为true减少重新计算大小次数。父布局大小改变而导致 RecyclerView 调整不受此参数限制。

1
recyclerView.setHasFixedSize(true)

3.2 mCachedViews

RecyclerView 离屏缓存 mCachedViews 默认为2,即视图移出屏幕后,放到共享缓存池前缓存2个元素,目的优化慢速向前、向后滑动的抖动。

1
recyclerView.setItemViewCacheSize(4)

按照个人经验判断这个值设置超过4效果不明显。

3.3 setHasStableIds

指定参数为true,避免增删items时引起无关的视图刷新。

1
adapter.setHasStableIds(true)

当然还需要重写方法获取id计算结果。

1
2
3
override fun getItemId(position: Int): Long {
    return items[position].id.hashCode().toLong()
}

四、参考链接