一文看懂 Compose 强制跳过模式(Strong Skipping Mode)

TD;LR

Jetpack Compose 自 1.5.4 起引入了强制跳过模式(Strong Skipping Mode,简称强跳模式),强跳模式的开启可以有效提升 Compose 重组性能。

什么是强跳模式?这里先给一个总结,后文详细阐述。一旦开启强跳模式,意味着:

  • Composable 函数的参数无论是否稳定类型,都可以通过参数比较跳过重组
  • 稳定类型参数使用 Object.equals 比较,不稳定类型通过 === 进行比较
  • Lambda 如果捕获了不稳定类型,依然可以被记忆

从智能重组谈起

我们知道 Compose 的重组是其声明式 UI 运转的基础,过于频繁的重组会影响渲染性能,好在 Compose 的重组非常智能,经过编译器处理后的 Composable 会被插入参数比较代码,在重组过程中通过参数diff,识别不必要的重组(参数没有变化时)跳过以提升渲染性能。

类型稳定性

强跳模式出现之前,跳过重组有个必要条件,即参与diff的参数类型必须是稳定性类型。

关于什么是稳定性类型,可以参考官方定义

developer.android.com/develop/ui/…

也可以参考我的另一篇文章

Compose 类型稳定性注解:@Stable & @Immutable

稳定类型应该是 immutable 的类型(例如 String 或者仅有 val 成员的 data class),亦或者其变化可被追踪的类型(mutableStateOf() 的返回值 ),而且稳定类型的成员必须也是稳定类型。

需要注意 Lambda 在 Compose 中也是稳定类型,即可以参与参数比较。这对于理解强跳模式的作业也是一个关键知识点,后文会介绍到

为什么不稳定类型不能跳过重组?

例如一个有 var 成员的 data class,当 var 成员变化时,重组中无法通过 diff 发现实例的变化,此时本着错杀一千不放过一起的原则,Compose 干脆将其视为不稳定类型,不生成 diff 代码,也无法跳过重组,以确保渲染的正确。

如果我们有信心确保一个不稳定的类型不会发生预期外的变更,可以通过 @Stable@Immutable 注解将其声明为稳定类型,这样,接收其作为参数的 Composable 仍然可以通过 diff 跳过重组。

为什么要有强跳模式?

不稳定类型强制参与重组,这个初衷是好的,通常功能正确比性能更重要。但是这也给广大开发者带来了困惑,Compose 社区有无数反馈都是关于 “为什么参数没有变化但仍然触发了重组?”。

为了跳过不必要的重组,开发者们需要弄懂稳定类型、不稳定类型等复杂概念,需要添加 @Stable 让不稳定类型跳过重组,对于那些三方库的类,无法添加注解的还要思考更 trick 的方法来染过“不稳定类型无法跳过重组”这个规则限制。

根据二八原则,不稳定类型的状态被篡改可能只是少数,但是连累八成没问题的 case 也无法跳过重组,开发者们为了为了提升性能要付出更多编码成本,有些本末倒置。

一个好的框架,应该让开发者用最低的成本和最符合直觉方式完成八成的工作,剩余少数的边界情况再 case by case 解决。因此,Compose 引入了强跳模式,强跳模式放宽了“可跳过重组的”限制,Composable 的参数中即使有不稳定类型参数,也会参与重组中的diff。参数 diff 过程中

  • 不稳定类型与 Composition 存储的历史值做 === 比较
  • 稳定类型则跟以前一样使用 Object.equals() 比较

当所有参数 diff 均返回 false 时,Composable 会跳过重组。

注意:正确理解“强制跳过模式” 的意思,并非强制 Composable 跳过重组,而是强制 Composable 的参数参与是否重组的比较,具体是否跳过重组要根据比较结果来看

如何开启强跳

强跳模式最早可使用版本是 Jetpack Compose Compiler 1.5.4,彼时可以作为实验功能开启:

tasks.withType() {
    compilerOptions.freeCompilerArgs.addAll(
        "-P",
        "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true",
    )
}

随着功能的 Stable 以及 Compose Compiler 合入 Kotlin 仓库, 自 Kotlin 2.0.0 起,可以像下面这样开启

android { ... }

composeCompiler {
   enableStrongSkippingMode = true
}

注意从 Kotlin 2.0.2 之后,强跳模式会默认开启,上面配置省略也会生效。

强跳模式的副作用

如前所述,强跳模式是照顾了八成case的开发效率,而牺牲了二成case的正确性。需要注意有的 case 在强跳开启前是正常的,强跳开启后可能就错误了,举个例子

@Composable
fun MyToggle(enabled: Boolean) {}
@Composable
fun MyList(list: List<String>) {}

@Composable
fun MyScreen() {
   var list by remember { mutableStateOf(mutableListOf("Foo")) }
   var toggle by remember { mutableStateOf(false) }
   MyToggle(toggle)
   MyList(list)

   Button(
      onClick = {
         list.add("Bar")
         toggle = !toggle
      }
   ) { Text("Toggle") }
}

当点击 Button 时,list 本身发生变化,但是由于 === 比较的是同一个引用,diff 返回 false,会跳过重组,list 的变化无法被正确刷新。

所以要务必注意,我们开启强跳,意味着我们有信息识别少数case的发生,甚至避免这类case发生。

也许有人会问不稳定类型通过 === 比较如果返回 false,就意味着一定不相等吗?答案是不一定,这里也是出于覆盖大多数原则的考虑,通常情况下我们不会对 mutable 类型使用拷贝构造相同值的多个实例,实例不同就代表值不同。即便错杀了一两个无辜的 case 也无伤大雅。

不稳定 Lambda 及其在强跳模式下的变化

强跳模式除了让不稳定类型参与diff,还有一个重要变化是“不稳定 lambda 可以被记忆(remember)”。

前面提到了 lambda 是稳定类型,不稳定 lambda 的叫法有点不严谨,这里的不稳定 lambda 即捕获(引用了外部变量)了不稳定类型的 lambda。

@Composable
fun Sample(
    viewModel: MainViewModel 
){
    SampleImpl(
        onClick = { viewModel.onClicked() },
    )
}     

如上,onClick 中捕获的 viewModel 是个不稳定类型,因此 onClick 是个不稳定 lambda。

对于捕获了稳定类型的 lambda,Compose Compiler 会自动为其包装一个 remember ,避免 lambda 因重新创建实例而在重组中 diff 失败。这很重要,试想一下很多 Composable 都有 lambda 类型的参数(例如 onClick 等),如果 lambda 不被记忆,意味着大部分 Composable 都无法跳过重组。

历史上,对于不稳定 lambda 则不会自动包装 remember,因为不稳定类型的存在让 lambda 所在的 Composable 无法跳过重组,因此自动包装 remember 也没必要了。

那么强跳模式改变了什么?

强跳模式开启下,不稳定类型也会参与 diff,Composable 也不一定跳过重组,所以不稳定 lambda 也要随之改变,跟其他所有 lambda 一样都会被包装 remember,上面的例子经过 Compiler 处理后,会变为下面这样:

@Composable
fun Sample(
    viewModel: MainViewModel 
){
    SampleImpl(
        onClick = remember(viewModel){ 
           { viewModel.onClicked() }
        },
    )
}     

捕获的外部变量 viewModel 变为 remember 的 key。 Key 采用同样的比较规则,即不稳定类型地址比较,稳定类型值比较。

另外,不捕获任何外部变量的 Lambda 也是会被记忆的,这不需要 Compose 编译器特别处理,Kotlin 编译器默认就是这样的行为,会为 Lambda 创建一个静态单例。对于 Composable 的 lambda 也是永远都会被记忆。

效果验证

我们可以使用 Compose Compiler Metrics 来验证开启强跳模式的效果

github.com/JetBrains/k…

The Compose Compiler plugin can generate reports / metrics around certain compose-specific concepts that can be useful to understand what is happening with some of your compose code at a fine-grained level.

这是 Compose Compiler 自带的工具,可以对 Compiler 处理后的 Compose 代码做一个分析报告

可见,强跳开启前后,可跳过 Composable 和 Labmda 的数量都有所增加。

还需要使用 @Stable 吗

开启了强跳模式,相当于弱化了不稳定类型和稳定类型的区别,以后还需要使用 @Stable 声明稳定类型吗?

还是需要的,不稳定类型使用 === 比较,这里会存在风险。如果我们想要使用值比较替代地址比较,还是要依靠 @Stable 或者 @Immutable

对于哪些在第三方库中无法添加注解的类型,可以使用稳定性配置文件(stability configuration files),使用方法可以参考

developer.android.com/develop/ui/…

总结

强制跳过模式是一个小更新,但是其实影响深远,它有点像从“性本恶”到“性本善”的转变,认为不稳定类型的危害是可忽略的,当然它的开启会引入正确性风险,这也是为什么它经过了长期的实验验证后,才正式引入。经过实验和开发者反馈,证明它的引入是收到欢迎的。只要大家遵循好的编码习惯,不随意滥用不稳定性类型,鼓励打开强跳模式,会让 App 性能得到一个整体提升。

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/22493,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?