Bootstrap

Jetpack Compose 性能优化的几条建议

关于 LazyColumn 优化性能的几条建议

第一个就是列表中如果有加载图片,那么大图一定要压缩!一定要重采样!不要几M几十M的图直接放到app里面加载!那样会卡爆!会内存OOM!

我们可以使用三方的图片加载库,如coil,因为图片加载库一般会缓存,同一张图片不会重复加载第二次,还可以根据尺寸压缩等。

关于 coil 图片加载库的使用,请参考 Jetpack Compose中的Accompanist 一文中有介绍

第二个,为 LazyColumnitems() 方法显示的指定一个 key ,可以优化列表的加载性能,避免多余的重组。尤其是对于长列表而言性能更佳。

LazyColumn(
    modifier = Modifier.fillMaxSize(),
    contentPadding = PaddingValues(16.dp),
    verticalArrangement = Arrangement.spacedBy(16.dp)
) {

    items(items = images, key = { it.id }) { image ->
        ImageDetails(image)
    }
}

这里的 key 需要指定一个唯一的id,如果列表的业务数据没有提供一个唯一的id,可以使用UUID生成一个:

data class MyImage(
    val resId: Int, 
    val title: String, 
    val tags: List<String>,
    val id: String = UUID.randomUUID().toString() // 自己生成一个唯一id
)

第三个,可以为数据类添加@Immutable以提高性能,这样编译器会认为它是稳定的类型,从而可以实施智能重组、跳过重组等特性。

@Immutable
data class MyImage(
    val resId: Int,
    val title: String,
    val tags: List<String>,
    val id: String = UUID.randomUUID().toString()
)

上面代码如果不加@Immutable,我们在 Layout Inspector 中观察列表快速滑动的时候,会发现列表出现红色闪烁,这说明发生了重组。因为其中的 val tags: List<String>不是一个稳定的类型,因为编译器不知道它会如何实现。@Immutable向编译器承诺:我是一个稳定的类型。加了它之后,在 Layout Inspector 中就可以看到重组次数为0,跳过重组的次数有很多(本该重组的次数)。

使用 @Stable 优化重组

来看一个简单示例:

class MyActivity: ComponentActivity() { 

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyComposeApplicationTheme {
                var selected by remember { mutableStateOf(false)}
                Column {
                    Checkbox(
                        checked = selected,
                        onCheckedChange = { selected = it }
                    )
                    ContactsList(
                        isLoading = false,
                        names = listOf("Peter")
                    )
                }

            }
        }
    }

    @Composable
    fun ContactsList(
        isLoading: Boolean,
        names: List<String> // 问题在这里
    ) {
        Box(contentAlignment= Alignment.Center) {
            if (isLoading) {
                CircularProgressIndicator()
            } else {
                Text(names.toString())
            }
        }
    }
}

上面代码中同样是由于names: List<String>不是一个稳定的类型,会导致重组。当我们点击 Checkbox 时,不仅 Checkbox 自身会重组,而且不读取selected 状态值的 ContactsList 也会重组。如果我们在 Layout Inspector 中观察,可以明显看到这个效果:

在这里插入图片描述

解决办法是将不稳定类型包装进一个数据类,并为其添加@Stable注解,代码修改后如下:

class MyActivity: ComponentActivity() {  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyComposeApplicationTheme {
                var selected by remember { mutableStateOf(false)}
                Column {
                    Checkbox(
                        checked = selected,
                        onCheckedChange = { selected = it }
                    )
                    ContactsList(
                        state = ContactListState(
                            isLoading = false,
                            names = listOf("Peter")
                        )
                    )
                }

            }
        }
    }

    @Composable
    fun ContactsList(
        state: ContactListState
    ) {
        Box(contentAlignment= Alignment.Center) {
            if (state.isLoading) {
                CircularProgressIndicator()
            } else {
                Text(state.names.toString())
            }
        }
    }

    @Stable
    data class ContactListState(
        val isLoading: Boolean,
        val names: List<String>
    )
}

这时如果我们再次在 Layout Inspector 中观察,就可以看到与之前不同的效果:

在这里插入图片描述

这次可以明显的看到,当我们点击 Checkbox 时,只有 Checkbox 自身会重组,ContactsList 不会再重组了,并且我们可以看到 Checkbox的重组次数是 5,而 ContactsList 跳过重组的次数是 5

当然这里你也可以使用 @Immutable 来注解数据类,目前它跟@Stable没有太大的区别。

另一个解决方法是使用 ImmutableList,像下面这样:

class MyActivity: ComponentActivity() { 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyComposeApplicationTheme {
                var selected by remember { mutableStateOf(false)}
                Column {
                    Checkbox(
                        checked = selected,
                        onCheckedChange = { selected = it }
                    )
                    ContactsList(
                        isLoading = false,
                        names = persistentListOf("Peter")
                    )
                }

            }
        }
    }

    @Composable
    fun ContactsList(
        isLoading: Boolean,
        names: ImmutableList<String> // 使用不可变List
    ) {
        Box(contentAlignment= Alignment.Center) {
            if (isLoading) {
                CircularProgressIndicator()
            } else {
                Text(names.toString())
            }
        }
    } 
}

注意使用 ImmutableList需要单独添加一个依赖:

implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.6") 

如果你对本文提到的 List<String> 是一个不稳定类型这一点存有疑惑,请参考我的另一篇文章:Jetpack Compose 深入探索系列二:Compose 编译器,在这篇文章中的类的稳定性推断部分,详细的介绍了稳定类型需要满足的准则,以及什么样的类型是稳定类型或者不稳定类型。

如果你需要更加详细全面的了解 Jetpack Compose 中的性能优化,请参考我的另一篇文章:
Jetpack Compose 中的重组作用域和性能优化

;