自从Jetpack Compose作为Google推出的现代化UI工具包以来,它在Android开发领域掀起了不小的浪潮。Compose的声明式、响应式让UI构建变得前所未有的简洁高效。
然而,在大量数据场景下,Compose内置列表组件LazyColumn
的性能却一直受到质疑和探讨。本博客将深入分析LazyColumn
的工作原理,揭示它遇到的性能瓶颈,并介绍一些优化手段,最后与经典的RecyclerView进行性能对比。
LazyColumn
工作原理:LazyColumn
是Compose中用于呈现大量重复数据的列表组件,其设计理念借鉴了Bruce法则:数据驱动 虚拟化、离屏回收等技术。具体来说,LazyColumn
会根据用户可视区域,只渲染极少部分实际可见的内容。当滚动时,Compose会智能回收离开屏幕的已渲染组合项,并重新组合新出现的项目,从而大幅节省内存和CPU开销。
然而,这种基于状态跟踪的响应式重组机制,在处理大规模列表时仍可能遇到一些瓶颈和性能问题。
性能问题分析,通过Traceview等诊断工具,我们可以详细分析LazyColumn
在大数据场景下的性能表现。主要发现了以下几个方面的问题:
-
重组开销大,当列表数据发生变更时,
LazyColumn
需要重新组合受影响的项目。大规模列表意味着更多项目需要重组,从而带来较大的CPU和内存开销。 -
内存抖动问题,由于Kotlin和Compose都是基于JVM的,因此也会遇到经典的GC和内存抖动问题。大数据列表增加了这方面的风险。
内存抖动(Memory Churn)指的是在运行应用程序时,内存的使用情况存在剧烈的波动,出现内存的急剧分配和释放的情况。这种
-
绘制效率低,由于Compose需要在低层次API上反复执行测量和布局操作,这会影响最终渲染速度。
RecyclerView
的ViewHolder
模型在此有一定优势。 -
项目复杂度影响
LazyColumn
可复用性有限,对于需要复杂状态跟踪的列表项,组合项本身就可能成为性能瓶颈。
针对这些问题,Compose和社区提出了一些优化方案和建议。
一、优化方案
- 使用
Stable
声明,利用kotlinx.collections.immutable
包提供的List
、Map
等稳定集合,配合Compose 1.2引入的@Stable
注解,可以避免意外的重组发生。
kotlin
@Composable
fun MyItem(@Stable items: List<Item>) {
// ...
}
-
状态提升,复杂列表项内部状态跟踪也可能带来性能问题。因此建议将状态提升到更高级别进行集中管理,以避免底层地的重组。
-
进阶的虚拟化技术Compose 1.3引入的
LaxyLayoutPolicy API
,可以为LazyColumn
、LazyRow
等定制精细化的虚拟化策略,充分发挥虚拟化优势。
kotlin
fun pageVirtualizationPolicy(pageSize: Int) = LazyLayoutPolicy {
it.policyFor(pageSize = pageSize)
}
-
惰性计算与缓存,对于计算代价昂贵的数据转换或UI组件,可采用延迟加载和缓存等策略,避免重复计算。
-
限制重组范围Compose提供的
DisposableEffect
、SideEffects
等API,可用于限制重组的影响范围,避免不必要的重组。 -
合理使用
Animations
,合理使用Jetpack Compose动画API,可以保证动画流畅性,且不会对其他UI区域造成影响。
7.选择性使用ViewsInCompose
,对于无法用Compose实现的特殊UI需求(如自定义View),可以考虑将其包装进AndroidView
,并于Compose集成使用。
二、LazyColumn与RecyclerView对比
与RecyclerView
的性能对比,经过多轮的性能测试和数据采集,总结出LazyColumn
和RecyclerView
在大数据列表场景下的一些性能差异:
- 内存占用:
LazyColumn
通常比等效的RecyclerView
实现占用更多内存,主要是由于Compose本身的内存模型所致,不过差距在合理的范围内。 - CPU开销:
LazyColumn
渲染时的CPU使用率略高于等效的ViewHolder
模型。这是由Compose底层的计算和跟踪机制造成的。 - 第一次渲染耗时:
LazyColumn
第一次渲染列表时的耗时较长,因为需要组合大量项目。而ViewHolder
则只需找到对应的View对象并设置数据。 - 后续滚动流畅度:滚动过程中,两者的表现接近,都能保持较好的流畅度。不过列表项越复杂,
LazyColumn
的优势就越小。 - 视图层级和测量布局:在复杂布局场景下,
ViewHolder
的扁平视图层级结构可以带来一定优势,避免了内部的测量和布局开销。 - 定制灵活性:相比ViewHolder的高度自由,Compose目前在定制复杂列表视图仍有些局限性,需要谨慎权衡。
- 开发体验:在开发体验层,Compose更易于编写、维护和测试,具有响应式编程和声明式UI描述等优势。ViewHolder则需要编写更多样板代码。
总的来说,LazyColumn
在性能方面并不比RecyclerView
差,而且还有很大的优化空间。实际开发中,我们需要结合具体场景权衡利弊,对于简单列表来说,LazyColumn
是更好的选择,但复杂场景下ViewHolder
的优势仍然存在。
三、优化Example
下面我将基于最新的Compose 1.6.3版本和Material 3,提供了一个LazyColumn
性能优化示例。该示例将包含以下几个部分:
- 使用不可变集合
- 使用key避免重复渲染
- 状态提升
- 惰性计算和缓存
- 限制重组范围
- 合理使用动画
- 选择性使用ViewsInCompose
kotlin
package com.example.jetnotes.ui.screens
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.view.View
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
fun <T> MutableList<T>.toggle(element: T) = if (contains(element)) remove(element) else add(element)
data class MyItem(val id: Int, val name: String)
data class MyItemDetails(val id: Int, val details: String)
class MyViewModel: ViewModel() {
private val _expandedIds = mutableStateListOf<Int>()
val expandedIds: List<Int> = _expandedIds
private val listeners = mutableListOf<(Int) -> Unit>()
fun toggleExpanded(id: Int) {
_expandedIds.toggle(id)
notifyListeners(id)
}
fun updateExpandedIds(id: Int) = _expandedIds.toggle(id)
fun addListener(listener: (Int) -> Unit) = listeners.add(listener)
fun removeListener(listener: (Int) -> Unit) = listeners.remove(listener)
private fun notifyListeners(id: Int) {
listeners.forEach { it(id) }
}
}
@Composable
fun OptimizedLazyColumnDemo(
items: List<MyItem> = List(100) { MyItem(it, "Item $it") },
viewModel: MyViewModel = viewModel()
) {
val stableItems = items.toList() // 1.使用不可变集合
LazyColumn(
content = {
itemsIndexed(stableItems) { _, item ->
key(item.id) { // 2.使用key避免重复渲染
val isExpanded = viewModel.expandedIds.contains(item.id)
MyItemView(
item = item,
isExpanded = isExpanded,
onExpandedChange = {
viewModel.toggleExpanded(item.id)
}
)
}
}
}
)
}
@Composable
fun MyItemView(
item: MyItem,
isExpanded: Boolean,
onExpandedChange: (Int) -> Unit
) {
val transition = updateTransition(targetState = isExpanded, label = "MyItemViewTransition")
val height by transition.animateDp(label = "heightTransition") {
if (it) 200.dp else 50.dp // 6.合理使用动画
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
onClick = { onExpandedChange(item.id) }
) {
Column(
modifier = Modifier
.padding(16.dp)
.height(height)
) {
Text(text = item.name, style = MaterialTheme.typography.titleLarge, color = Color.Red)
if (isExpanded) {
Spacer(modifier = Modifier.height(8.dp))
val details = remember(item.id) { calculateDetails(item.id) } // 4.惰性计算与缓存
DisposableEffect(key1 = item.id, effect = { // 5. 限制重组范围
// 执行一些副作用操作
onDispose { }
})
AndroidView(
factory = { context ->
MyCustomView(context) // 7.选择性使用ViewsInCompose
},
update = { view ->
view.setData(details)
}
)
}
}
}
}
fun calculateDetails(id: Int): MyItemDetails {
// 执行一些昂贵的计算操作
return MyItemDetails(id, "Details for item @id")
}
class MyCustomView(context: Context): View(context) {
private var data: MyItemDetails? = null
private val paint = Paint()
init {
paint.textSize = 40f
}
fun setData(details: MyItemDetails) {
data = details
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
data?.let {
canvas.drawText("ID: ${it.id}", 0f, 50f, paint)
canvas.drawText("Details: ${it.details}", 0f, 100f, paint)
}
}
}
@Preview(showBackground = true)
@Composable
fun OptimizedLazyColumnDemoPreview() {
MaterialTheme {
OptimizedLazyColumnDemo()
}
}
以下是对各部分代码的详细解释:
- 使用不可变集合
我们使用items.toList()
将传入的List<MyItem>
转换为不可变列表。这样可以确保在后续重组过程中,列表本身不会被意外修改,从而避免不必要的重组。
- 使用Key避免重复渲染
在LazyColumn
的item
作用域内,我们使用key(item.id)
为每个项目指定了唯一的key。这可以避免相同数据的项目被记重复渲染,从而减少不必要的计算开销。
- 状态提升
我们将列表项的展开/折叠状态提升到MyViewModel
中进行集中管理。这样可以避免在每个列表内部都维护自己的状态,从而减少层次的重组。
- 惰性计算和缓存
对于计算代价昂贵的calculateDetail
操作,我们使用remember
函数将结果缓存起来。当相同的item.id
再次出现时,就可以直接从缓存中获取结果,避免重复计算。
- 限制重组范围
通过LaunchedEffect
和key1
参数,我们限制了副作用操作的重组范围。只有当item.id
发生变化时,相关的骨作用才会被重新执行,从而避免了不必要的重组。
- 合理使用动画
使用updateTransition
和animateHeight
实现了一个简单的展开/折叠动画效果。这种方式可以确保动画流畅,且不会影响其他UI区域的性能。
- 选择性使用ViewsInCompose
对于无法用Compose实现的自定义View,我们使用AndroidView
将其包装进来,并与Compose集成使用。这种混合使用方式可以发挥两者的优势,提高灵活性。
展示效果
通过以上多种优化手段的综合运用,可以大幅提升LazyColumn在大数据场景下的性能表现,确保流畅的用户体验。不过,需要注意的是,这些优化策略也需要根据具体场景进行权衡,因为它们可能会增加一些发开复杂度。
如有更好的优化思路,欢迎前来交流~