Jetpack Compose 深入探索系列二:Compose 编译器

Jetpack Compose由一系列的库组成,但我们需要重点关注三个特定的库:Compose compilerCompose runtimeCompose UI

其中 Compose编译器Compose runtime 是Jetpack Compose的支柱。从技术上讲,Compose UI 不是Compose体系结构的一部分,因为运行时和编译器被设计为通用的,并由符合其需求的任何客户端库使用。Compose UI 只是其中一个可用的客户端。还有其他的客户端库正在开发中,比如JetBrains为桌面和Web开发的客户端库。也就是说,浏览Compose UI将帮助我们理解Compose如何提供可组合树的运行时内存表示,以及它最终如何从其中物化真正的元素。

在这里插入图片描述

到目前为止,我们已经了解到Compose编译器和Compose runtime一起工作来解锁所有的库特性,但这可能仍然有点太抽象了,因为我们还不太足够熟悉它。关于Compose编译器会采取什么操作使我们的代码符合runtime要求,runtime如何工作,何时触发初始组合和进一步的重组,如何在内存中提供树的表示,如何将这些信息用于进一步的重组……等等,我们可能希望得到更深入的解释。掌握这样的概念可以帮助我们在编写代码时对库的工作方式和期望有一个全面的认识。

Kotlin编译器插件

Jetpack Compose有点依赖于代码生成。在Kotlin和JVM的世界中,通常的方法是通过kapt实现注解处理器,但是Jetpack Compose不同于此。Compose编译器实际上是一个Kotlin编译器插件。这使得库能够将其编译工作嵌入到Kotlin的编译阶段,从而获得关于代码形式的更多相关信息,并加快整个过程。虽然kapt需要在编译之前运行,但编译器插件可以直接内联到编译过程中

作为一个Kotlin编译器插件也提供了在编译器的前端阶段报告诊断异常的机会,提供了一个非常快速的反馈循环。但是,这些诊断不会在IDE中得到报告,因为IDEA没有直接与插件集成。我们今天在Compose中可以找到的任何IDEA级别的检查都是通过一个单独的IDEA插件添加的,这个插件不与Compose编译器插件共享任何代码。也就是说,只要我们按下编译按钮,前端诊断就会被报告。改进反馈循环是Kotlin编译器前端阶段静态分析的最终好处,Jetpack Compose编译器很好地利用了这一点。

Kotlin编译器插件的另一个巨大优势是,它们可以随意调整现有的源代码(不只是像注解处理器那样添加新代码)。它们能够在这些元素被降级为更底层的产物之前,修改它的输出 IR ,进而可以将其转换为目标平台支持的原语(记住Kotlin是支持多平台的)。这将使Compose编译器能够根据runtime的需要来转换Composable函数

编译器插件在Kotlin中有着前途无量的未来。许多已知的注解处理器可能会通过KSP逐渐迁移为编译器插件或“轻量级”编译器插件。

如果你对Kotlin编译器插件特别感兴趣,我强烈建议你学习一下KSP (Kotlin符号处理器),谷歌提议将其作为Kapt的替代品。KSP提出了一种规范化的DSL,用于“编写轻量级编译器插件”,任何库都可以依赖它进行元编程。如果你对KSP感兴趣的话,可以参考我的博文:Kotlin 元编程之 KSP 全面突破 以及 Kotlin 元编程之 KSP 实战:通过自定义注解配置Compose导航路由

另外,请注意,Jetpack Compose编译器在很大程度上依赖于IR转换,如果将其作为元编程的广泛实践,可能会有危险。如果所有的注解处理器都被翻译成编译器插件,我们可能会有太多的IR转换,类似的事情可能会破坏语言的稳定。调整/扩充语言总是有风险的。总的来说,这就是为什么KSP可能是一个更好的选择。

Compose 注解

我们首先需要了解的一件事情是如何注解代码,以便编译器能够扫描所需的元素并发挥它的魔力。让我们从了解可用的Compose注解开始学习。

即使编译器插件相比注解处理器可以做更多的事情,两者也有一些共同之处。这方面的一个例子是它们的前端编译阶段,经常用于静态分析和验证。

Compose编译器利用kotlin编译器前端的钩子/扩展点来验证它想要强制执行的约束是否满足,类型系统是否正确处理了@Composable函数、声明或表达式。

除此之外,Compose还提供了其他补充注解,用于在某些特定情况下解锁额外的检查和各种运行时优化或“快捷方式”。所有可用的注解都是由Compose runtime库提供的。

所有的Jetpack Compose注解都是由Compose runtime提供的,因为编译器和runtime模块都很好地利用了这些注解。

@Composable

Compose编译器注解处理器之间最大的区别在于,Compose可以有效地更改被注解的声明或表达式。大多数注解处理器无法做到这一点,它们必须生成额外的或者同级声明。这就是Compose编译器使用IR转换的原因。@Composable注解实际上改变了事物的类型,并且编译器插件被用于在前端阶段强制执行各种规则,以确保Composable类型不会被视为与非Composable的注解类型对等。

通过@Composable改变声明或表达式的类型会给它一个“内存”。这就是调用 remember 和利用Composer/slot table的能力。它也提供了一个生命周期,使得在其函数体内启动的副作用能够遵从生命周期( eg:跨越重组的作业)。Composable函数还将被分配一个它们将保留的标识,并在生成的树中有一个位置,这意味着它们可以将Node节点发射到Composition组合树中。

简要回顾:Composable函数表示从数据到节点的映射,该节点在执行时被发送到树中。这个节点可以是一个UI节点,也可以是任何其他性质的节点,这取决于我们用来使用Compose runtime的库。Jetpack Compose runtime使用不绑定到任何特定用例或语义的泛型节点类型。

@ComposeCompilerApi

Compose使用这个注解来标记它的某些部分,这些部分只能由编译器使用,其唯一目的是通知潜在用户这一事实,并让他们知道应该谨慎使用它。

@InternalComposeApi

在Compose中,有些api被标记为内部的,因为即使公开的api接口保持不变并冻结到稳定版本,它们也会在内部发生变化。这个注解的范围比语言的internal关键字更广,因为它允许跨模块使用,而Kotlin不支持这个概念。

@DisallowComposableCalls

用于防止在函数内部发生可组合调用。这对于组合函数的内联lambda参数非常有用,因为它们不能安全地在其中包含可组合调用。它最好用于lambdas,因为lambdas不会在每次重组时都被调用。

这方面的一个例子可以在Compose runtime的remember函数中找到。这个Composable函数记住由计算块产生的值。此块仅在初始组合期间计算,任何进一步的重新组合将始终返回已经生成的值。

在这里插入图片描述
由于这个注解,在calculation lambda中禁止Composable调用。如果允许,那么它们将在调用(发射)时占用 slot table 中的空间,并且在第一次组合之后将被丢弃,因为lambda不再被调用。

该注解最好用于作为实现细节有条件调用的内联lambdas,但不应该像可组合元素那样是“活的”。之所以需要这样做,是因为内联lambda的特殊之处在于它们“继承”了其父调用上下文的可组合能力。例如,forEach调用的lambda没有标记为@Composable,但是如果forEach本身是在可组合函数中调用的,则可以调用可组合函数。这在forEach和许多其他内联api的情况下是需要的,但在其他一些情况下不需要,如remember,这就是这个注解的用途。

还需要注意的是,这个注解具有“传染性”,即如果你在标记为@DisallowComposableCalls的内联lambda中调用内联lambda,编译器将要求你将该lambda也标记为@DisallowComposableCalls

正如你可能猜到的那样,这可能是一个你永远都不会在任何客户端项目中使用到的注解,但如果你将Jetpack Compose用于除了 Compose UI 外的其他场景,那么它很可能会变得更加有意义。在这种情况下,你可能需要为runtime编写自己的客户端库,这将要求你遵守运行时约束。

@ReadOnlyComposable

当其应用于一个可组合函数时,这意味着我们知道这个可组合函数的主体永远不会写入composition,只会从它读取。对于主体中的所有嵌套Composable调用也必须保持如此。这允许runtime避免生成不需要的代码,如果Composable能够满足这个假设。

对于任何写入内部composition的Composable,编译器会生成一个“group”来包装它的主体,因此整个group在runtime被触发。这些发出的group为composition提供了关于Composable的必要信息,因此当重新组合需要用不同Composable的数据覆盖它时,它知道如何清除任何已写入的数据,或者如何通过保留Composable的标识来移动这些数据。可以被生成的group有着不同的类型:例如:可重新启动的组,可移动的组……等等。

要了解group究竟是什么,可以想象在选定文本的给定范围的开始和结束处有一对指针。所有组都有一个源码位置的key,用于存储group,从而解锁位置记忆。这个key也是它如何知道在ifelse分支条件逻辑之间的不同标识,例如:

在这里插入图片描述
它们都是文本,但它们具有不同的标识,因为它们对调用者而言表示不同的意思。可移动的组也有一个语义标识Key,因此它们可以在父group中重新排序。

当我们的可组合对象不写入composition时,生成这些组不会提供任何值,因为它的数据不会被替换或移动。这个注解有助于避免这种情况。

在Compose库中,关于只读组合的例子可能是许多CompositionLocal默认值或委托它们的utilities,如 Material Colors, TypographyisSystemInDarkTheme()函数,LocalContext,任何关于获取应用resources类型的调用(因为它们依赖于LocalContextLocalConfiguration)。总的来说,它是关于在运行我们的程序时只设置一次的东西,并且希望保持不变,并且可以从树上的Composables中读取。

@NonRestartableComposable

当其应用于函数或属性getter时,它基本上使其成为一个不可重新启动的Composable。
(注意,默认情况下不是所有的组合都是可重新启动的,因为内联组合或具有非Unit返回类型的组合都是不可重新启动的)。

添加该注解时,编译器不会生成允许函数重组或在重组期间跳过所需的引用。请记住,这必须非常谨慎地使用,因为它可能只对非常小的函数有意义,这些函数可能会被重新组合(重新启动)由另一个调用它们的Composable函数调用,因为它们可能包含很少的逻辑,所以对它们来说自我失效没有多大意义。换句话说,它们的无效/重组本质上是由它们的父组件/封闭组件驱动的。

为了“准确性”,应该很少或永远不需要这个注解,但如果您知道这种行为将产生更好的性能,则可以将其用作非常轻微的性能优化。

@StableMarker

Compose runtime还提供了一些注解来表示类型的稳定性。它们是@StableMarker元注解,以及@Immutable@Stable注解。让我们从@StableMarker开始。

@StableMarker是一个元注解,它注解了其他的注解,比如@Immutable@Stable。这听起来可能有点多余,但它是为了可重用性,因此它的含义也适用于用它注解的所有注解。

@StableMarker暗示了与最终注解类型的数据稳定性相关的以下要求:

  • 对于相同的两个实例,无论何时调用equals的结果总是相同的
  • 当被注解的类型的公开属性发生变化时,总是会通知其Composition
  • 被注解的类型的所有公开属性也必须是稳定的

第一条要求实际上是在表达"现在相等以后就永远相等",第二条要求实际上意味着被注解类的所有公开的 var 属性应该使用 mutableStateOf() 来修饰。

任何用@Immutable@Stable注解的类型也需要隐含这些要求,因为这两个注解都被标记为@StableMarker,或者换句话说,它们都是作为稳定性的标记。

请注意,这些是我们给编译器的承诺,这样它就可以在处理源码时做出一些假设,但在编译时不会验证它们。这意味着您(开发人员)将决定何时满足所有需求。

也就是说,Compose编译器将尽力推断某些类型何时满足上述要求,并将这些类型视为稳定类型,而不进行注解。在许多情况下,这是首选的,因为它保证是正确的,然而,有两种情况下,直接注解它们是重要的:

  • 当它是接口或抽象类的必要契约/期望时。该注解不仅成为对编译器的承诺,而且成为了对实现者的要求。
  • 当实现是可变的,但在稳定性假设下以一种可变性是安全的方式实现时。最常见的例子是,如果类型是可变的,因为它有某种类型的内部缓存,但该类型对应的公共API与缓存的状态无关。
@Immutable

此注解应用于类上,作为编译器的严格承诺,即所有公共可访问的类属性和字段在创建后保持不变。注意,这是一个比语言val关键字更强的承诺,因为val只确保属性不能通过setter重新分配,但它可以指向一个可变的数据结构(例如val指向一个类对象,但该对象的数据内容字段可变)。这将打破Compose runtime的期望。换句话说,Compose之所以需要这个注解,本质上是因为Kotlin语言没有提供一种机制(关键字或其

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值