Compose 的重组会影响性能吗?聊一聊 recomposition scope

主题寄语:心理学家发现绿色能让心情放轻松android

很多初学 Compose 的同窗都会对 Composable 的 Recomposition(官方文档译为"重组")心生顾虑,担忧大范围的重组是否会影响性能。安全

其实这种担忧大可没必要, Compose 编译器在背后作了大量工做来保证 recomposition 范围尽量小,从而避免了无效开销:markdown

Recomposition skips as much as possible
When portions of your UI are invalid, Compose does its best to recompose just the portions that need to be updated.
developer.android.com/jetpack/com…app

那么当重组发生时,其代码执行的范围到底是怎样的呢?咱们经过一个例子来测试一下:ide

@Composable
fun Foo() {
    var text by remember { mutableStateOf("") }
    Log.d(TAG, "Foo")
    Button(onClick = {
        text = "$text $text"
    }.also { Log.d(TAG, "Button") }) {
        Log.d(TAG, "Button content lambda")
        Text(text).also { Log.d(TAG, "Text") }
    }
}
复制代码

如上,当点击 button 时,State 的变化会触发 recomposition。函数

请你们思考一下此时的日志输出是怎样的oop

image.png 。。。。性能

你能够在文章末尾找到答案,与你的判断是否一致呢?学习


Compose 如何肯定重组范围?

Compose 在编译期分析出会受到某 state 变化影响的代码块,并记录其引用,当此 state 变化时,会根据引用找到这些代码块并标记为 Invalid 。在下一渲染帧到来以前 Compose 会触发 recomposition,并在重组过程当中执行 invalid 代码块。测试

Invalid 代码块即编译器找出的下次重组范围。可以被标记为 Invalid 的代码必须是非 inline 且无返回值@Composalbe function/lambda,必须遵循 重组范围最小化 原则。

为什么是 非 inline 且无返回值(返回 Unit)?

对于 inline 函数,因为在编译期会在调用处中展开,所以没法在下次重组时找到合适的调用入口,只能共享调用方的重组范围。

而对于有返回值的函数,因为返回值的变化会影响调用方,所以没法单独重组,而必须连同调用方一同参与重组,所以它不能做为入口被标记为 invalid

范围最小化原则

只有会受到 state 变化影响的代码块才会参与到重组,不依赖 state 的代码不参与重组。

在了解 Compose 重绘范围的基本规则以后,咱们再回看文章开头的例子,并尝试回答下面的问题:


为何不仅是 Text 参与重组?

当点击 button 后,MutableState 发生变化,代码中惟一访问这个 state 的地方是 Text(...) ,为何重组范围不仅是 Text(...) ,而是 Button {...} 的整个花括号?

首先要理解出如今 Text(...) 参数中的 text 其实是一个表达式

下面两中写法在执行顺序上是等价的

println(“hello” + “world”)
复制代码
val arg = “hello” + “world”
println(arg)
复制代码

老是 “hello” + “world” 做为表达式先执行,而后才是 println 方法的调用。

回到前面的例子,参数 text 做为表达式执行的调用处是 Button 的尾lambda,然后才做为参数传入 Text()。 因此此时最小重组范围是 Button 的 尾lambda 而非 Text()


Foo 是否参加剧组 ?

按照范围最小化原则, Foo 中没有任何对 state 的访问,因此很容易知道 Foo 不该该参与重组。

有一点须要注意的是,例子中 Foo 经过 by 的代理方式声明 text,若是改成 = 直接为 text 赋值呢?

@Composable fun Foo() {
  val text: MutableState<String> = remember { mutableStateOf("") }

  Button(onClick = { 
  	 text = "$text $text"
  }) {
    Text(text.value)
  }
}
复制代码

答案是同样的,仍然不会参与重组。

第一,Compose 关心的是代码块中是否有对 state 的 read,而不是 write

第二,这里的 = 并不意味着 text 会被赋值新的对象,由于 text 指向的 MutableState 实例是永远不会变的,变的只是内部的 value


为何 Button 不参与重组?

这个很好解释,Button 的调用方 Foo 不参与重组,Button 天然也不会参与重组,只有尾 lambda 参与重组便可。


Button 的 onClick是否参与重组?

重组范围必须是 @Composable 的 function/lambda ,onClick 是一个普通 lambda,所以与重组逻辑无关。


注意!重组中的 Inline 陷阱!

前面讲了,只有 非inline函数 才有资格成为重组的最小范围,理解这点特别重要!

咱们将代码稍做改动,为 Text() 包裹一个 Box{...}

@Composable
fun Foo() {

    var text by remember { mutableStateOf("") }

    Button(onClick = { text = "$text $text" }) {
        Log.d(TAG, "Button content lambda")
        Box {
            Log.d(TAG, "Box")
            Text(text).also { Log.d(TAG, "Text") }
        }
    }
}
复制代码

日志以下:

D/Compose: Button content lambda
D/Compose: Boxt
D/Compose: Text
复制代码

为何重组范围不是从Box开始?

ColumnRowBox 乃至 Layout 这种容器类 Composable 都是 inline 函数,所以它们只能共享调用方的重组范围,也就是 Button 的 尾lambda

若是你但愿经过缩小重组范围提升性能怎么办?

@Composable
fun Foo() {

    var text by remember { mutableStateOf("") }

	Button(onClick = { text = "$text $text" }) {
        Log.d(TAG, "Button content lambda")
        Wrapper {
            Text(text).also { Log.d(TAG, "Text") }
        }
    }
}

@Composable
fun Wrapper(content: @Composable () -> Unit) {
    Log.d(TAG, "Wrapper recomposing")
    Box {
        Log.d(TAG, "Box")
        content()
    }
}
复制代码

如上,自定义非 inline 函数,使之知足 Compose 重组范围最小化条件。


结论

Just don't rely on side effects from recomposition and compose will do the right thing -- Compose Team

关于重组范围的具体规则,官方文档中没有作详细说明。由于开发者只须要牢记 Compose 经过编译期优化保证了recomposition 永远按照最合理的方式运行,以最天然的方式开发就行了,无需针对这些具体规则付出额外的学习成本。

尽管如此,做为开发者仍要谨记一点:

不要直接在 Composable 中写包含反作用(SideEffect)的逻辑!

反作用不能跟随 recomposition 反复执行,因此咱们须要保证 Composable 的“纯洁性”。

你不能预设某个 function/lambda 必定不参与重组,于是在里面侥幸的埋了一些反作用代码,使其变得不纯洁。由于咱们没法肯定这里是否存在 “inline陷阱”,即便能肯定也不保证如今的优化规则在将来不会改变。

因此最安全的作法是,将反作用写到 LaunchedEffect{}DisposableEffect{}SideEffect{} 中,而且使用 remeber{}derivedStateOf{} 处理那些耗时的计算。



开头例子的运行结果:

D/Compose: Button content lambda
D/Compose: Text
复制代码
相关文章
相关标签/搜索