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

本文探讨了Compose中的Recomposition原理,解释了编译器如何确保范围最小化以提升性能。通过示例解析了代码执行范围,并提供了避免副作用和利用非inline函数优化重组范围的方法。关键点包括:编译期分析、范围最小化原则和Inline陷阱的规避。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

 

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

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…

那么当重组发生时,其代码执行的范围究竟是怎样的呢?我们通过一个例子来测试一下:

@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。

请大家思考一下此时的日志输出是怎样的

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{} 处理那些耗时的计算。

更多Android进阶知识,请看下方:

 


开头例子的运行结果:

D/Compose: Button content lambda
D/Compose: Text


 

<think>好的,用户想知道docker-compose现在是否还能用,特别是版本兼容性和可用性。首先,我需要确认当前docker-compose的版本情况。Docker Compose有两个版本,V1和V2,V2现在应该已经成为主流了,而且可能已经取代了V1。用户可能是在使用较新的Docker Desktop时自动安装了V2,而Linux用户可能需要手动安装。 接下来,版本兼容性方面,用户需要知道不同Docker Engine版本对应的Compose文件版本。例如,Compose文件版本3.8通常需要Docker Engine 19.03.0以上。如果用户使用的是旧版本的Docker,可能会遇到兼容性问题,这时候需要检查他们的Docker版本,可以通过docker --version查看,然后对照官方文档的兼容性表格。 然后,可用性方面,虽然V1已经不再维护,但V2作为插件集成到Docker CLI中,用户可以通过docker compose命令来使用,这可能会让些用户困惑,特别是之前习惯用docker-compose命令的用户。需要提醒他们注意命令的变化,比如从连字符变成空格。 另外,用户可能还关心如何检查自己的docker-compose版本,可以用docker-compose --version或者docker compose version来确认。如果是V2,后者更准确。同时,升级到最新版本通常能获得更好的兼容性和新功能,但也要注意现有的docker-compose.yml文件是否需要调整语法。 用户的问题可能还隐含着他们是否需要在现有项目中更改配置,或者是否有已知的兼容性问题。比如,某些旧版Compose文件中的语法可能在新版本中废弃,需要更新。此外,社区支持和插件生态也是可用性的部分,目前大部分工具和教程都已经转向V2,所以用户迁移后资源更多。 最后,要确保回答中引用了提供的参考资料,特别是关于docker版本和docker-compose的使用示例,比如引用中提到的docker-compose up -d命令,说明docker-compose依然可用,但需要注意版本差异。需要确认用户是否需要具体的步骤来升级或调整他们的环境,但根据问题,主要集中回答兼容性和可用性即可。</think>### Docker Compose的当前版本兼容性及可用性分析 #### 1. **Docker Compose的版本现状** Docker Compose目前分为两个主要版本:**V1**(原独立工具)和**V2**(集成到Docker CLI的插件模式)。自2023年起,Docker官方推荐使用**V2**,并将V1标记为“Legacy”(不再维护)[^1]。 - **V1**:通过独立的`docker-compose`命令运行,语法为`docker-compose up`。 - **V2**:作为Docker CLI插件,通过`docker compose`(注意空格)命令调用,例如`docker compose up`[^2]。 #### 2. **版本兼容性** Docker Compose的兼容性主要取决于**Docker Engine版本**和**Compose文件语法版本**: - **Docker Engine要求**: - V2需要Docker Engine 20.10.0及以上版本。 - 若使用旧版Docker(如19.03),需手动安装V1或升级Docker。 - **Compose文件语法**: - 语法版本3.x兼容Docker Engine 17.06及以上,例如`version: "3.8"`需Docker 19.03.0+[^2]。 #### 3. **可用性验证** 可通过以下步骤确认当前环境: ```bash docker --version # 查看Docker Engine版本(如20.10.12) docker compose version # 查看Docker Compose V2版本 docker-compose --version # 查看V1版本(若存在) ``` #### 4. **升级与迁移建议** - **从V1迁移到V2**: 1. 升级Docker Desktop至最新版(自动包含V2)。 2. Linux用户需手动安装插件: ```bash mkdir -p ~/.docker/cli-plugins curl -SL https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose chmod +x ~/.docker/cli-plugins/docker-compose ``` - **兼容性问题**: V2与V1的`docker-compose.yml`文件大部分兼容,但需注意部分命令差异(如`docker compose up`代替`docker-compose up`)[^2]。 #### 5. **应用场景示例** 使用V2启动服务(以Minio对象存储为例): ```yaml # docker-compose.yml services: minio: image: minio/minio command: server /data --console-address ":9001" ports: - "9000:9000" - "9001:9001" volumes: - minio_data:/data volumes: minio_data: ``` 启动命令: ```bash docker compose up -d # V2语法 # 或(若仍使用V1) docker-compose up -d # V1语法(已过时) ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值