Jetpack Compose -> 重组的性能风险和优化

本文讨论了JetpackCompose中的重组性能问题,特别是mutableStateOf引发的大量ReCompose可能导致资源浪费。文章介绍了如何通过避免无谓的函数执行和使用@Stable注解来优化性能,并强调了可靠类和不可靠类在ReCompose中的行为差异。
摘要由CSDN通过智能技术生成

前言


上一章我们讲解了 Jetpack Compose -> mutableStateOf 状态机制的背后秘密 本章我们讲解下重组的性能风险以及怎么优化;

重组的性能风险


前面我们一直在讲重组(ReCompose) 的过程,在使用 mutableStateOf() 以及对于 List 和 Map 在使用 mutatbleStateListOf()、mutableStateMapOf() 也能监听到内部状态变化,如果对于 List 和 Map 使用的是 mutableStateOf() 只能触发这个对象变化的监听;

重组其实分为:触发重组和重组,这是两个过程,触发重组是某个变量发生改变之后,Compose 去把已经组合好的那些部分重新的 Compose 一次,这个所谓的组合好的部分就是之前说的组合过程的结果;也就是那个稍后被拿去组合、测量、绘制的结果,它在相关的变量改变之后,是需要重新组合过程,重新生成结果的;

触发重组,就是 ReCompose Scope 重组范围;重组是在下一帧的时候去调用这些失效了的 compose 代码,来重新生成组合的过程;

因为 Column 函数是一个内联函数,所以 Column 函数在编译后会被抹除掉,也就是说,如果我们在使用下面的逻辑的时候

private var number by mutableStateOf(1)

@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        Android_VMTheme {
            Surface {
                println("Recompose 测试范围1")
                Column {
                    println("Recompose 测试范围2")
                    Text(text = "当前数值是: $number", modifier = Modifier.clickable {
                        number ++
                    })
                }
            }
        }
    }
}

Column 会被抹除掉,而是直接将 Text 放到那里,也就是说如果发生了 ReCompose 的时候,是会把 Column 前后范围内的都会触发 Recompose;

也就是说当我们点击 Text 触发 number++ 的时候,会再一次打印 “Recompose 测试范围1” 和 “Recompose 测试范围2”;

这其实就是重组的性能风险;一个小的改动,触发了大面积的 ReCompose,这就造成了计算资源浪费;

我们来继续验证下这个结论,假设我们有下面这样的一段代码

private var number by mutableStateOf(1)

@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        Android_VMTheme {
            Surface {
                println("Recompose 测试范围1")
                Column {
                    testPerformance()
                    println("Recompose 测试范围2")
                    Text(text = "当前数值是: $number", modifier = Modifier.clickable {
                        number ++
                    })
                }
            }
        }
    }
}


@Composable
fun testPerformance() {
    println("Recompose 测试范围 performance")
    Text(text = "test")
}

按照上面的结论,当我们执行这段代码的时候,应该会打印 “Recompose 测试范围1” "Recompose 测试范围 performance 和 “Recompose 测试范围2”;

我们来运行看下,当我们点击 Text 的时候,发现并没有打印 “Recompose 测试范围 performance” 这一行输出,那么这是为什么呢?

难道这个 testPerformance 函数没有被调用吗?不是的,它被调用了,但是内部的逻辑没有执行,我们前面讲到过 Compose 的编译过程是由它的编译器 插件来干预的,这个干预过程会修改我们的 Composable 函数,会给函数增加一些参数,例如 Composer 函数会被添加进去,也会给你的函数添加一些条件判断进去,判断这个函数跟上一次调用传入的参数有没有改变,如果没有改变,就会跳过这个函数的内部执行逻辑,这是 Compose 的一种优化,它会避免在 ReCompose 的时候一些没必要的执行;

我们来看这个 testPerformance 函数,它在第二次被调用的时候,它的函数参数并没有发生变化,所以它内部的逻辑不会再执行;

如果我们给 testPerformance 函数增加一个参数

@Composable
fun testPerformance(text: String) {
    println("Recompose 测试范围 performance")
    Text(text = "test $text")
}

调用的时候,传入 number;

testPerformance(number.toString())

然后我们来重新执行一下,可以看到,结果是我们预期的结果了;Text 地方因为 number 的改变会被标记一次失效,同时 testPerformance 的调用地方因为也用到了 number,也会被标记一次失效,虽然标记了两次,但是这个是没有关系的,它只会执行一次重组,因为标记和重组是两个过程,它们是分开的,而且标记是个很轻的工作,它是不耗费什么计算资源的,所以不用担心性能,只会重组一次;

当传入一个 number 之后,这个 testPerformance 的重组就会从被动执行变成主动标记失效并执行的过程了;从执行角度来看是一样的,从标记角度来看,是从被动标记变成了主动标记的过程;

当 testPerformance 执行 ReCompose 的时候,Compose 发现它的函数变了,Compose 就会在第二次进入这个代码,所以打印就会执行;

这是 Compose 中很重要的一个性能优化点;那么问题来了,『这个性能优化是 Compose 相对传统 View 系统的写法的优势吗?』

答案显然不是的,传统写法是手动更新的,Compose 是自动更新的,而自动更新就会触发一个更新范围过大超过需求的问题,从而需要让你的框架去做这种跳过没有必要的更新的优化;这是针对过度更新的问题的优化,而不是相对传统 View 系统写法的优势

Compose 的重组在函数没有变化的时候,跳过函数的内部代码的执行,那肯定需要在 Recompose 的时候做一个对比,去比对 Compose 函数的值是否发生了改变,这个是否改变的判断,它是靠什么来判断的呢?

Structual Equality 结构性相等(Kotlin 的 ==)


这里额外提一个知识点,在 Kotlin 中 == 等于 Java 中的 equals,而 === 才等于 Java 的 ==;我们来验证下 Compose 在重组的时候是否是依赖的这种结构性相等来做出是否重组的决定;

我们来声明一个 data class;

data class User(val name: String)

testPerformance 修改如下:

@Composable
fun testPerformance(user: User) {
    println("Recompose 测试范围 performance")
    Text(text = "test $user.name")
}

调用的地方修改为

private var number by mutableStateOf(1)
var user = User("Mars")

@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        Android_VMTheme {
            Surface {
                println("Recompose 测试范围1")
                Column {
                    testPerformance(user)
                    println("Recompose 测试范围2")
                    Text(text = "当前数值是: $number", modifier = Modifier.clickable {
                        number ++
                        user = User("Mars")
                    })
                }
            }
        }
    }
}

当我们执行点击事件,number++的时候,我们给 user 重新赋了一次值,当 ReCompose 的时候,testPerformance(user) 就会使用这个新创建的 user,如果不是使用的结构性相等的话,那么就会执行 testPerformance 中的打印,如果使用的是结构性相等,则不会打印;

我们运行看下,可以看到,并没有打印 testPerformance 中的日志,说明 Compose 在 ReCompose 使用的是结构性相等来判断是否要重组;

可靠的类 & 不可靠的类

我们接下来做另一个改动,把 name 的修饰符改成 var,其他地方不做改动;

data class User(var name: String)

我们来运行看下,可以看到,这次直接打印了 testPerformance 中的日志,说明 Compose 在 ReCompose 的时候发生了重组;

那么这是为什么呢?因为当我们使用 var 修饰符的时候,Kotlin 就认为这个类不可靠了,对于可靠的类,Compose 使用结构性相等来判断是否发生了改变,对于不可靠的类,Compose 就不判断,直接进入 Composeable 函数的内部,无脑执行了;

那么,问题来了,为什么一个 var 关键字就把这个类变成了不可靠的类了呢?

我们先来把上面这段代码做一个小小的改动;

private var number by mutableStateOf(1)
val user1 = User("Mars")
val user2 = User("Mars")
var user = user1

@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        Android_VMTheme {
            Surface {
                println("Recompose 测试范围1")
                Column {
                    testPerformance(user)
                    println("Recompose 测试范围2")
                    Text(text = "当前数值是: $number", modifier = Modifier.clickable {
                        number ++
                        user = user2
                    })
                }
            }
        }
    }
}
data class User(var name: String)

我们来创建两个新的 User 分别是 user1 和 user2,这样写对于程序执行的结果是不变的, 当我们点击 Text 的时候,testPerformance 在 ReCompose 的时候也会改变,它的参数从 user1 变成了 user2,这个时候在 ReCompose 的时候,是会进入这个 testPerformance 函数内部的,因为 User 类的 name 是 var 类型的,一个神奇的存在,var 就会让其重组,我们假设它不会发生重组的场景下来推演下它会发生什么;

假设在重组的过程中,认为这个 user 没有改变,理由是它们通过 equals 判断认为是同一个对象,所以认为没变,就跳过了 testPerformance 这个函数的内部执行,这样显示不会出问题,但是如果在这之后,程序又在其他地方执行了一些其他逻辑,从其他地方把 user2 的 name 的值做了修改,这个是不会触发重组的,因为 user1 和 user2 并没有使用 mutableStateOf,但是如果又由于其他原因触发了 ReCompose 的行为,但是不是从外面这种捎带着往里的触发 testPerformance 的重组,而是直接触发了 testPerformance 内部的重组,那么当它内部独立的发生了 ReCompose 的时候,它内部显示的文字是不会改变的,因为它内部始终监听的是 user1 对象,虽然在
点击事件中 testPerformance(user1) 的 user1 被替换成了 user2 但是内部并没有发生重组,也就是对于 testPerformance 内部来说,它一直观测的都是 user1 这个旧的值,这样的话就算 testPerformance 内部发生了独立的 ReCompose,并且它应该观测的 user2 的 name 值也发生了改变,但是它内部的显示并不会去显示这个 user2 的 name 的最新值,而是显示那个本来应该被抛弃的 user1 的 name 的值;

简单来说:当 testPerformance 在 ReCompose 的时候,参数里面的 user 换成一个新的对象的时候,如果 Compose 用 equals 判断出来新对象和老对象是相等的,那就不进入 testPerformance 的内部代码了,虽然当下没有显示问题,并且看似节约了性能,但是会导致函数内部的后续的监听全部失效了,都监听了错的老的对象,而没有监听正确的新的对象,造成未来的显示问题;

所以 当我们使用 var 的时候,Compose 就无脑的直接进入了,它认为这个发生了改变;val 关键字修饰的字符串它的值是不可以修改的,而 User 类它的内部只有 name 这一个属性,如果这个 name 是不可变的,那么这个创建出来的 User 对象也是不会改变的;

也就是说:如果能保证现在相等,以后也相等,那么就不进入,如果不能保证现在相等,以后也相等,那么就无脑进入;

到这的时候,可能好多人就有疑问了,这个优化岂不是根本用不到,我们大部分使用的是 var 类型,还是会造成性能损耗;这其实是一个存在了好久的问题;

我们都使用过 HashMap,它在 put 元素的时候,通过 hashCode() 和 equals() 来判断 key 的冲突,当我们使用一个对象作为 key 的时候:

HashMap<User, String> hashMap = new HashMap();
User user = new User("Mars"); // 自己实现了 hashCode() equals()
hashMap.put(user, "1")

当我们使用一个对象作为 key 的时候,一定要确定它的 hashCode 值是不可变的,如果我们使用的是一个 data class,它的 hashCode 和 equals 都是和它内部的值有关系的,如果我们使用一个 data class 来作为 key 就要保证它的值是不可变的,

@Stable


那么针对上面的问题,我们怎么解决呢? Compose 提供了一种方案,『@Stable』注解,如果你给 User 类添加这么一个注解,Compose 在 ReCompose 的时候就会跳过;

@Stable
data class User(var name: String)

这个 @Stable 是一个稳定性标记,加上这个注解就是在告诉 Compose 编译器插件,这个类型是可靠的,不用检查,由人工来保证;

但是人工保证并不能做到绝对,程序还是可能会出现问题,那么我们怎么处理呢?就是让它不相等,也就是我们不去重写 equals 方法,而是采用它本身的 equals 逻辑;

@Stable
class User(var name: String)

当我们使用的是同一个对象的时候,可以让 Compose 编译器插件不执行检查;

private var number by mutableStateOf(1)
// val user1 = User("Mars")
// val user2 = User("Mars")
var user = User("Mars")

@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        Android_VMTheme {
            Surface {
                println("Recompose 测试范围1")
                Column {
                    testPerformance(user)
                    println("Recompose 测试范围2")
                    Text(text = "当前数值是: $number", modifier = Modifier.clickable {
                        number ++
                        // user = user2
                    })
                }
            }
        }
    }
}

当我们点击 Text 的时候,没有给 user 重新赋值,但是我们使用 『@Stable』注解标记了这个 User 类,那么就不会执行内部的检查;

另外,@Stable 的稳定,需要满足下面三点

  1. 现在相等就永远相等;
  2. 当公开属性改变的时候,要通知到用这个属性的 Composition
  3. 公开属性,也必须全都是可靠类型,或者说稳定类型

那么怎么通知到 Composition 呢?很简单,就是通过 mutableStateOf;

@Stable
class User(name: String) {
    var name by mutableStateOf(name)
}

针对第三点,我们来看下面的代码

class Company(var address: String)

class User(name: String, company: Company) {
    var name by mutableStateOf(name)
    var company by mutableStateOf(company)
}

因为 company 是一个不稳定的属性,所以它就会导致 User 成为一个不稳定的属性,哪怕是使用了 mutableStateOf;

而 Compose 只会判断第二条,只要满足第二条,它就会认为稳定;

好了,今天的 Compose 就到这里吧;

下一章预告


derivedStateOf 与 remember() 有什么区别?

欢迎三连


来都来了,点个关注点个赞吧,你的支持是我最大的动力~~~

  • 12
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android Jetpack Compose 中,可以将点击和拖动的功能分别封装成 Clickable 和 Draggable 两个 Composable 函数,并将它们组合起来使用,实现同时实现点击和拖动的效果。 Clickable Composable 函数用于监听点击事件,返回一个布尔值表示是否消耗了点击事件。 ```kotlin @Composable fun Clickable( onClick: () -> Unit, children: @Composable () -> Unit ) { val state = remember { mutableStateOf(false) } val gesture = remember { MutableInteractionSource() } val clickableModifier = Modifier.pointerInput(Unit) { detectTapGestures( onPress = { state.value = true }, onRelease = { if (state.value) onClick() }, onCancel = { state.value = false }, onTap = { state.value = false } ) gesture.tryAwaitRelease() } Box( modifier = clickableModifier, propagateMinConstraints = true, propagateMaxConstraints = true ) { children() } } ``` Draggable Composable 函数用于监听拖动事件,返回拖动后的位置。 ```kotlin @Composable fun Draggable( state: DragState, onDrag: (Offset) -> Unit, children: @Composable () -> Unit ) { val offsetX = state.position.x val offsetY = state.position.y val density = LocalDensity.current.density val draggableModifier = Modifier.pointerInput(Unit) { detectDragGestures( onDragStart = { state.isDragging = true }, onDragEnd = { state.isDragging = false }, onDrag = { change, dragAmount -> state.position += dragAmount / density onDrag(state.position) change.consumePositionChange() } ) } Box( modifier = draggableModifier.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }, propagateMinConstraints = true, propagateMaxConstraints = true ) { children() } } class DragState(var isDragging: Boolean = false, var position: Offset = Offset.Zero) ``` 使用 Clickable 和 Draggable 组合实现同时点击和拖动的效果。 ```kotlin @Composable fun ClickableAndDraggable( onClick: () -> Unit, onDrag: (Offset) -> Unit, children: @Composable () -> Unit ) { val state = remember { DragState() } Draggable( state = state, onDrag = onDrag, children = { Clickable( onClick = onClick, children = children ) } ) } ``` 调用 ClickableAndDraggable 函数即可实现同时点击和拖动的效果。 ```kotlin var position by remember { mutableStateOf(Offset.Zero) } ClickableAndDraggable( onClick = { /* 处理点击事件 */ }, onDrag = { p -> position = p } ) { Box( Modifier .background(Color.Red) .size(50.dp) ) { Text("Drag me!", Modifier.align(Alignment.Center)) } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值