要小心点,不要掉 Recomposition 带来的性能坑了

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 今天我们就来聊聊可能因为没理解透 Recomposition 而写出的性能问题。

Jetpack Compose 写起 UI 来毋庸置疑的是快。它的接口设计简单,但是涉及了很多平时不会用到的新概念,很多问题都可能是没搞懂这些概念而产生的。


今天我们就来聊聊可能因为没理解透 Recomposition 而写出的性能问题。


首先来看一段代码:

@Composable
fun TestContent() {
    val scrollState = rememberScrollState()
    Column(
        modifier = Modifier
            .fillMaxSize()
    ) {
        Header(scroll = scrollState.value)
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .weight(1f)
                .background(Color.White)
                .verticalScroll(scrollState)
        ) {
            for(i in 0 until 500){
                Text(text = "scroll content")
            }
        }
    }
}
@Composable
fun Header(scroll: Int) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(150.dp)
            .background(Color.Gray)
    ){
        Text(
            modifier = Modifier.align(Alignment.Center),
            text = "scroll = $scroll",
            fontSize = 17.sp
        )
    }
}

TestContent 是个 Column, 分为上边的 Header 和下边的滚动容器, Header 会显示滚动偏移量。


我们将这段代码跑起来,可以看到结果是完全符合预期。

c7e5102c90be5681c1621d35d2c32b8.png

那是否就没有问题了呢?


这段代码是存在性能问题的:因为 Header 需要 scrollState.value 这个数据,所以我们在 TestContent 里读取 scrollState.value,然后传递给 Header, 而因为 scrollState.value 是一个 state,所以它的值变化时会触发访问函数的 Recomposition,在这里相当于框架调用了一次 TestContent,所以每次滚动,TestContent 就会被调用数次,那个 500 次的循环也会不断地跑,这个循环每次跑出来结果是一样的,就是浪费了。如果数据再多,那么就是滚动卡顿了。


那有没有方式来发现这种问题呢?Android Studio Dolphin 的 Layout Inspector 提供了支持:

68510b0e4c6d84b5382da871c11a083.png

可以通过 Layout Inspector 看到在我滚动的过程中, TestContentrecomposed 了 18 次, 而滚动容器下都显示 skip 了 18 次,这是说循环都跑了,但是每个循环里的 Text 参数一致,所以忽略了,这也是框架已有的优化。


如果不是高版本的 Android Studio,那怎么发现这些问题呢?那只能用万能的日志大法了。

@Composable
fun TestContent() {
    val scrollState = rememberScrollState()
    Log.i("test", "enter test content")
    Column(
        modifier = Modifier
            .fillMaxSize()
    ) {
        Header(scroll = scrollState.value)
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .weight(1f)
                .background(Color.White)
                .verticalScroll(scrollState)
        ) {
            for(i in 0 until 500){
                Log.i("test", "enter loop $i")
                Text(text = "scroll content")
            }
        }
    }
}

在这两个点加上日志,我们滚动时,就可以看出源源不断的日志输出了。

那我们应该怎么优化呢?


上面已经提到,Recomposition 是函数级别的,所以我们把 scrollState.value 的访问放到 Header 里去,就不会有问题了,所以在使用 Jetpack Compose 时,很有必要去将页面划分成一个个微小的组件,使得 Recomposition 的范围尽量可控,而不要去写那种很长而且依赖的状态值很多的函数。


在例子中的情况,我们可以把 scrollState 整个传递进去,但更好的做法是官方推荐的 lambda 表达式参数传递。

// Header 传参由 Int 变为 ()-> Int
@Composable
fun Header(scroll: () -> Int) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(150.dp)
            .background(Color.Gray)
    ){
        Text(
            modifier = Modifier.align(Alignment.Center),
            text = "scroll = ${scroll()}",
            fontSize = 17.sp
        )
    }
}
@Composable
fun TestContent() {
    val scrollState = rememberScrollState()
    Column(
        modifier = Modifier
            .fillMaxSize()
    ) {
        Header(scroll = {
            scrollState.value
        })
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .weight(1f)
                .background(Color.White)
                .verticalScroll(scrollState)
        ) {
            for(i in 0 until 500){
                Text(text = "scroll content")
            }
        }
    }
}

这样,我们再次用 Layout Inspector 查看:

7dc60a7c6b6d943323d6ea8260ea20f.png

这样子被 recomposed 就只有 Header 了,如果我们用日志的方式,那么滚动时也不会有日志输出了。界面性能又提升了一丢丢。


正所谓解决复杂的问题,往往只需要简单的几行代码,最终还是要落实到原理的理解上。


最后,提个小问题来,来加深大家对 Composable 函数的理解,假设我的代码是这样子的:

@Composable
fun TestContent() {
    val scrollState = rememberScrollState()
    Column(
        modifier = Modifier
            .fillMaxSize()
    ) {
        Header(scroll = scrollState.value)
        Content(scrollState)
    }
}
@Composable
fun Header(scroll: Int) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(150.dp)
            .background(Color.Gray)
    ){
        Text(
            modifier = Modifier.align(Alignment.Center),
            text = "scroll = $scroll",
            fontSize = 17.sp
        )
    }
}
@Composable
fun ColumnScope.Content(scrollState: ScrollState){
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .weight(1f)
            .background(Color.White)
            .verticalScroll(scrollState)
    ) {
        for(i in 0 until 500){
            Text(text = "scroll content")
        }
    }
}

上面的代码,我只是把滚动容器抽取到一个单独的函数中,那么循环在滚动时还会不断地被执行吗? 为什么?

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
存储 编译器
深入解析i++和++i的区别及性能影响
在我们编写代码时,经常需要对变量进行自增操作。这种情况下,我们通常会用到两种常见的操作符:i++和++i。最近在阅读博客时,我偶然看到了有关i++和++i性能的讨论。之前我一直在使用它们,但从未从性能的角度考虑过,这让我突然产生了兴趣。尽管它们看起来相似,但它们之间存在微妙而重要的区别。在本文中,我们将详细解释i++和++i之间的区别,以及它们对代码性能的影响。
393 1
深入解析i++和++i的区别及性能影响
|
1月前
|
存储 C#
【C#】大批量判断文件是否存在的两种方法效率对比
【C#】大批量判断文件是否存在的两种方法效率对比
42 1
|
1月前
|
监控 Java
G1垃圾回收器的哪些配置参数对性能影响最大,如何调整这些参数
G1垃圾回收器的哪些配置参数对性能影响最大,如何调整这些参数
|
3月前
|
存储 缓存 JSON
一行代码,我优化掉了1G内存占用
这里一行代码,指的是:String.intern()的调用,为了调用这一行代码,也写了几十行额外的代码。
|
Java 调度
服务器常见问题排查(一)——cpu占用高、上下文频繁切换、频繁GC
文章主要讨论了服务器中常见性能问题的一些排查思路,这篇文章主要讨论了CPU负载过高,频繁GC和频繁切换上线文这三个问题。
1279 0
服务器常见问题排查(一)——cpu占用高、上下文频繁切换、频繁GC
|
SQL Java Linux
一不留神就掉坑
一不留神就掉坑
57 0
一个UE频繁掉网的问题
这个UE频繁掉网的问题,其实蛮low的,熟悉的人,看一个参数值就搞定这个问题了,但是还是做个记录。问题背景是运营商指定UE锁在某个NR小区,在一个区域的弱信号点(RSRP -110dbm左右)进行TPUT测试,但是最后发现UE在-106 dbm左右时就会掉网,没办法进行测试。测试反馈:UE锁在NR N41 520110/344小区上,一开始可以正常进行TPUT,随着往弱信号的方向上移动,UE就会出现掉网。
|
消息中间件 关系型数据库 Shell
记录贴:sentry磁盘占用过大如何清理?
记录贴:sentry磁盘占用过大如何清理?
1220 0
|
存储 NoSQL 测试技术
rediskey值内存消耗以及性能影响
rediskey值内存消耗以及性能影响
195 0
|
Java
频繁 YGC的一段代码
假期重新把之前在新浪博客里面的文字梳理了下,搬到这里。
377 0