Compose Strong Skipping Mode 的真相:它并不会让你的类型变 Stable

开篇:一个让我困惑了三天的 Bug

上周五我在项目里遇到一件诡异的事------一个列表页面的 item 明明传了相同的数据,却在每次滚动时都触发 recomposition。

我第一反应是:"不可能啊,我开了 Strong Skipping Mode 啊。"

然后打开 Layout Inspector,发现 recomposition count 疯涨。接着我又看了下 Compose Compiler Metrics 的报告,豁然开朗------我的 data class 根本不是 Stable 的。

等等,Strong Skipping Mode 不是应该"让所有参数都能被跳过"吗?它不是解决了 Stability 问题吗?

不是

这个误解在社区里流传之广,甚至让很多团队关掉了 Stability 优化的代码审查流程。今天我们就来彻底聊清楚:Strong Skipping Mode 到底做了什么,以及它没做什么。

什么是 Strong Skipping Mode?

先回到基础。Compose 的核心优化机制之一是 Skipping:如果一个 Composable 函数的参数没有变化,编译器就可以跳过它的 recomposition。

但在 Strong Skipping Mode 出现之前,编译器只会在参数类型被标记为 @Stable@Immutable 时才会生成跳过逻辑。如果你传入了一个"不稳定"的类型(比如包含 var 属性或外部模块的类),编译器直接放弃------每次 recomposition 都会执行这个 Composable。

Strong Skipping Mode(从 Compose Compiler 1.5.4 开始实验,2026年已默认开启)改变了规则:

• 对于 Stable 类型 的参数:使用 equals() 比较(和以前一样)

• 对于 Unstable 类型 的参数:使用 ===(引用相等/identity)比较

也就是说,即使参数类型是 Unstable 的,只要你传入的是同一个对象引用,Compose 照样可以跳过 recomposition。

误解在哪里?

关键问题来了。很多人的理解是:

"开了 Strong Skipping Mode = 所有类型都自动变成 Stable = 不需要再关心 Stability"

真相是:

"Strong Skipping Mode 只是改变了比较策略,让 Unstable 类型也有机会被跳过------但用的是引用比较,不是值比较"

这区别太大了。来看一个具体例子:

kotlin 复制代码
// 这个 data class 在外部模块定义,Compose 编译器推断它为 Unstable
data class UserProfile(
    val name: String,
    val avatar: String,
    val lastSeen: Instant  // java.time.Instant 来自外部模块
)

@Composable
fun ProfileCard(profile: UserProfile) {
    // ...
}

在没有 Strong Skipping Mode 时,ProfileCard 永远不会被跳过。

有了 Strong Skipping Mode 后,编译器生成的逻辑变成:

scss 复制代码
// 伪代码:编译器生成的跳过判断
if (profile === previousProfile) {  // 注意:是 === 不是 ==
    skip()
}

问题在于:在实际业务中,你几乎不可能每次传入同一个引用。一旦你从 ViewModel 中 collect 一个 StateFlow,每次 emit 新值时,即使内容完全相同,也是一个新对象:

kotlin 复制代码
// ViewModel 中
private val _profile = MutableStateFlow(UserProfile(...))

fun refreshProfile() {
    // 即使数据没变,copy() 也会创建新引用
    _profile.value = _profile.value.copy()
    // 或者从网络重新拉取,即使数据一样也是新对象
    _profile.value = api.getProfile()
}

这就是为什么我的列表在开了 Strong Skipping Mode 后还是疯狂 recompose------每个 item 的数据虽然值相同,但引用不同。

equals() vs === :性能差异有多大?

你可能会想:"那为什么不对所有类型都用 equals() 呢?"

因为 Compose 团队无法信任你的 equals() 实现。

@Stable 注解是一个契约,它承诺:

• 如果两个实例的 equals() 返回 true,那么它们对 Composition 的影响完全相同

• 如果属性发生变化,Composition 一定会被通知到(通过 State/MutableState)

没有这个契约,贸然用 equals() 可能导致 UI 不一致------一个被篡改了内部状态但 equals 仍返回 true 的对象,会让 Compose 错误地跳过更新。

举个极端例子:

kotlin 复制代码
// 危险!不满足 @Stable 契约
class BadUser(val name: String) {
    var cachedBitmap: Bitmap? = null  // 可变字段,但不参与 equals

    override fun equals(other: Any?): Boolean {
        return other is BadUser && other.name == name
    }
}

如果 Compose 信任这个 equals(),当 cachedBitmap 变化时 UI 就不会更新。所以对于 Unstable 类型,用 === 是唯一安全的选择。

实际影响:Strong Skipping Mode 帮了谁?

说了半天它"没做到什么",那它到底帮了谁?

场景一:Lambda 参数

这是 Strong Skipping Mode 最大的受益者。在旧模式下,每个 lambda 在 recomposition 时都会被视为"新值"(因为 lambda 每次都会创建新实例)。Strong Skipping Mode 引入了 lambda 的 remember 优化

scss 复制代码
// 你写的代码
Button(onClick = { viewModel.doSomething() }) {
    Text("Click")
}

// 开启 Strong Skipping 后,编译器自动包装
Button(onClick = remember { { viewModel.doSomething() } }) {
    Text("Click")
}

这意味着你不再需要手动 remember 每个 lambda 来避免子组件 recompose。这是一个巨大的质量提升。

场景二:单例对象或长期持有的引用

scss 复制代码
// 这种场景下 === 比较有效
val theme = LocalMyTheme.current  // CompositionLocal 提供的同一个引用
MyWidget(config = theme.widgetConfig)  // 只要 theme 没变,就不会 recompose

场景三:remember 包裹的对象

scss 复制代码
val decorationStyle = remember { DecorationStyle(color = Red, width = 2.dp) }
// DecorationStyle 是 Unstable 的,但因为引用不变,Strong Skipping 有效
DecoratedBox(style = decorationStyle)

那我们该怎么做?2026 年的 Stability 最佳实践

既然 Strong Skipping Mode 不是银弹,我们在 2026 年应该怎么正确处理 Stability?

1. 继续关注 Compose Compiler Metrics

别因为开了 Strong Skipping 就关掉性能分析。在 build.gradle.kts 中配置:

ini 复制代码
composeCompiler {
    reportsDestination = layout.buildDirectory.dir("compose_reports")
    metricsDestination = layout.buildDirectory.dir("compose_metrics")
}

然后定期审查 *-classes.txt 文件中的 Unstable 标记,尤其是出现在列表/高频 recomposition 路径上的类。

2. 对高频路径的数据类做 Stability 优化

不需要对所有类都加 @Stable------这是过度优化。重点关注:

• LazyColumn/LazyGrid 的 item 数据类

• 频繁更新的 UI 状态类

• 作为参数传递给多个子 Composable 的 "大对象"

优化方式有几种:

kotlin 复制代码
// 方式一:直接标注(确保你满足契约)
@Stable
data class UserProfile(
    val name: String,
    val avatar: String,
    val lastSeen: Instant
)

// 方式二:使用 Stability Configuration File(推荐)
// compose-stability.conf
java.time.Instant
java.time.LocalDate
kotlinx.datetime.*

3. Stability Configuration File 是 2026 年的首选方案

比起在代码中到处加注解,使用配置文件更加优雅和可维护:

arduino 复制代码
// build.gradle.kts
composeCompiler {
    stabilityConfigurationFile = rootProject.layout.projectDirectory
        .file("compose-stability.conf")
}

// compose-stability.conf 内容
// 通配符表示该包下所有类视为 Stable
java.time.*
kotlinx.datetime.*
com.myapp.domain.model.*

这样你可以把外部模块的类型"声明"为 Stable,而不需要修改源码或添加注解。

4. 利用 derivedStateOf 和 remember 管理引用稳定性

kotlin 复制代码
// 错误:每次 recomposition 都创建新的 filteredList
@Composable
fun UserList(users: List<User>, query: String) {
    val filtered = users.filter { it.name.contains(query) }
    LazyColumn {
        items(filtered) { UserRow(it) }
    }
}

// 正确:使用 derivedStateOf 保持引用稳定
@Composable
fun UserList(users: List<User>, query: String) {
    val filtered by remember(users, query) {
        derivedStateOf { users.filter { it.name.contains(query) } }
    }
    LazyColumn {
        items(filtered) { UserRow(it) }
    }
}

5. 状态提升时注意粒度

这是最近 ProAndroidDev 上讨论的热点。状态放在哪一层直接影响 recomposition 的范围:

kotlin 复制代码
// 粗粒度:整个 Screen 都会 recompose
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    // state 变化 → 整个 Screen recompose
    Header(state.name, state.avatar)
    Stats(state.followers, state.posts)
    Bio(state.bio)
}

// 细粒度:只有变化的部分 recompose
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
    val name by viewModel.name.collectAsStateWithLifecycle()
    val avatar by viewModel.avatar.collectAsStateWithLifecycle()
    val followers by viewModel.followers.collectAsStateWithLifecycle()
    // 每个 State 独立,只影响对应的子 Composable
    Header(name, avatar)
    Stats(followers, viewModel.posts.collectAsStateWithLifecycle().value)
    Bio(viewModel.bio.collectAsStateWithLifecycle().value)
}

当然,过细的粒度会让代码变复杂。我的建议是:对高频变化的字段做拆分,低频的合在一起就好

实战:用 Layout Inspector 验证优化效果

说了这么多理论,来看看怎么验证。Android Studio Panda 4 的 Layout Inspector 现在可以直接显示 recomposition count 和 skip count:

• 打开 Layout Inspector → 选择你的 Compose 界面

• 顶部开启 "Show Recomposition Counts"

• 操作 UI(滚动列表、切换页面)

• 观察数字:高 recomposition + 低 skip = 需要优化

另外,新版 Studio 的 Composition Tracing 也很好用------它能直接在 System Trace 中标注每个 Composable 的执行时长,让你精确定位性能瓶颈。

一张图总结决策路径

当你遇到 recomposition 性能问题时,按这个顺序排查:

第一步:确认 Strong Skipping Mode 已开启(2026年默认开启,但老项目可能没有)

第二步:用 Compose Compiler Metrics 找到 Unstable 的类

第三步:判断这些类是否在高频路径上

第四步:如果是------用 Stability Config File 或 @Stable 注解修复

第五步:检查是否有不必要的对象创建(remember / derivedStateOf)

第六步:考虑状态粒度拆分

写在最后

Strong Skipping Mode 是一个非常好的"兜底"优化。它把 Compose 的性能下限提高了一大截------特别是在 lambda 处理方面,几乎消除了一整类的性能陷阱。

但它不是你放弃思考 Stability 的理由。

在列表场景、高频 UI 更新、复杂状态管理这些真正需要性能的地方,理解 ===equals() 的区别、知道什么时候该介入优化,仍然是 Compose 开发者的必修课。

工具在进步,但对底层机制的理解永远不会过时。

下次有人跟你说"开了 Strong Skipping 就不用管 Stability 了",你可以把这篇文章甩给他 :)

如果这篇文章对你有帮助,欢迎点赞、收藏、转发。关注我,获取更多 Android 新技术深度解析。

相关推荐
千里马学框架15 小时前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台15 小时前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone15 小时前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc16 小时前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo17 小时前
Kotlin 协程中的 Job 结构化并发与取消
android
sun00770018 小时前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼19 小时前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone20 小时前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen20 小时前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone20 小时前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui