Jetpack Compose `@ReadOnlyComposable` 的“魔法”

什么是 @ReadOnlyComposable

在 Compose 中,普通 @Composable 函数不仅负责绘制 UI,还会参与 Composition 的状态管理。每次调用通常都会在内部 Slot Table 中创建一个新的 Group(组合节点),用于:

  • 跟踪函数调用位置
  • 管理状态
  • 支持 Recomposition
  • 实现 Skip(跳过未变化部分)

@ReadOnlyComposable 本质上是一种特殊优化,它向 Compose 编译器做出承诺:

"我只是读取上下文数据,不生成 UI,不创建新的组合节点,也不会产生副作用。"

常见场景:

kotlin 复制代码
@Composable
@ReadOnlyComposable
fun currentDensity(): Density {
    return LocalDensity.current
}

它只是读取 CompositionLocal 的值。

普通 @Composable 会发生什么

例如:

kotlin 复制代码
@Composable
fun UserThemeColor(): Color {
    return MaterialTheme.colorScheme.primary
}

编译器在背后大致会变成:

kotlin 复制代码
fun UserThemeColor(
    composer: Composer,
    changed: Int
): Color {

    composer.startReplaceableGroup(...)
    
    val color = MaterialTheme.colorScheme.primary

    composer.endReplaceableGroup()

    return color
}

这里:

  • startReplaceableGroup()
  • endReplaceableGroup()

会创建 Composition Group。这样 Compose 才能记录:

  • 当前函数位置
  • 状态依赖
  • 是否可以跳过重组
@ReadOnlyComposable 的变化

加上:

kotlin 复制代码
@Composable
@ReadOnlyComposable
fun UserThemeColor(): Color {
    return MaterialTheme.colorScheme.primary
}

编译器会省略 Group 创建:

kotlin 复制代码
fun UserThemeColor(
    composer: Composer,
    changed: Int
): Color {

    return MaterialTheme.colorScheme.primary
}
差异

普通 Composable:

kotlin 复制代码
startGroup()
读取状态
endGroup()

ReadOnlyComposable:

kotlin 复制代码
读取状态

少了:

  • Group 分配
  • Slot Table 写入
  • 重组跟踪成本

这就是它的性能收益来源

为什么 Compose 官方会使用它

在 Material3 源码里能看到类似:

kotlikn 复制代码
val ColorScheme.primary: Color
    @Composable
    @ReadOnlyComposable
    get() = LocalColorScheme.current.primary

原因:主题颜色可能被频繁访问:

kotlin 复制代码
Text(
    color = MaterialTheme.colorScheme.primary
)

Button(
    colors = ButtonDefaults.buttonColors(
        containerColor = MaterialTheme.colorScheme.primary
    )
)

如果每次都创建新的 Group:

  • Slot Table 会变大
  • 重组跟踪更多节点
  • 性能增加额外开销

@ReadOnlyComposable 避免了这些成本。

什么时候不能使用

下面这种是错误示例:

kotlin 复制代码
@Composable
@ReadOnlyComposable
fun Wrong() {
    Text("Hello")
}

原因:会创建 UI 节点。

scss 复制代码
Text()

再比如:

kotlin 复制代码
@Composable
@ReadOnlyComposable
fun Wrong() {
    val state = remember {
        mutableStateOf(0)
    }
}

原因:

remember

  • 写入 Slot Table
  • 创建状态节点

这违反了"只读"的承诺。

还有:

kotlin 复制代码
LaunchedEffect()
SideEffect()
DisposableEffect()

都不能放进去。

可以把它理解成什么

可以把:@Composable理解为:"我可能创建 UI,也可能管理状态。" 而@ReadOnlyComposable则是: "我只是读取 Compose 环境变量。" 它更像一个"零成本访问器(accessor)"。

实际开发建议

大部分业务代码几乎不需要自己写 @ReadOnlyComposable

适合的场景非常少:

  • 读取 CompositionLocal
  • 读取主题数据
  • 读取 Density
  • 读取 LayoutDirection
  • 读取 Context

例如:

less 复制代码
@Composable
@ReadOnlyComposable
fun screenWidth(): Dp {
    return LocalConfiguration.current.screenWidthDp.dp
}

不适合:

  • UI 组件
  • remember
  • Effect API
  • 状态修改
  • 动画逻辑

Compose 官方也主要把它用于框架内部的微优化,而不是日常业务开发

相关推荐
KevinWang_5 小时前
Android Hilt 依赖注入
android
古法安卓5 小时前
Android-深入理解 Android 回调(Callback)机制
android·性能优化·android studio
古法安卓5 小时前
Android-车机 GNSS 定位数据接收问题排查
android·java·android studio
FungLeo7 小时前
Flutter 吸顶分组列表实战:语义桶分组 + 点击头平滑滚动
android·flutter
阿巴斯甜8 小时前
Android 自定义权限
android
FungLeo9 小时前
Flutter 超长 StatefulWidget 拆分术:part of + extension on State 实战
android·flutter·dart
小王C语言10 小时前
MySQL 数据类型:数值类型、字符串类型、日期和时间类型、enum 和 set、find_in_set 查询
android·数据库·mysql
我命由我1234510 小时前
Android 控件 - CardView(快速实现圆角)
android·java·java-ee·kotlin·android studio·android-studio·android runtime
码云数智-园园10 小时前
Android 全方位性能优化合集
android·性能优化
hunterandroid11 小时前
[Android 从零到一] Compose 动画实战:从 AnimatedVisibility 到复杂转场与手势联动
android