@Compose 注解到底做了什么?了解一下

6799614c0eb5570672cefcdd10177d03.png

/   今日科技快讯   /

近日,由IDC、浪潮信息、清华大学全球产业研究院联合编制的《2021-2022全球计算力指数评估报告》在北京发布,量化揭示了全球主要国家GDP、数字经济与计算力之间的关联性和相互拉动作用。

评估结果显示,美国和中国分别以77分和70分位列前两位,同处领跑者位置;追赶者国家得分在40-55分区间,包括日本、德国、英国、法国等7国;得分低于40分的为起步者国家,包括印度、意大利、巴西等6国。报告指出,各样本国家所属阵营的划分较上一年未发生变化,全球各国算力格局已初步形成,美国和中国作为领跑者阵营国家,在全球算力领域的主导地位进一步得到了增强。

/   作者简介   /

周一好,最近气温变化很大,大家要注意增加衣服,新的一周继续加油吧!

本篇文章转自程序员江同学的博客,文章主要分享了他对Compose注解原理的相关理解,相信会对大家有所帮助!

原文地址:

https://juejin.cn/post/7051222120331739143

/   前言   /

了解过Compose的同学都知道,只需要添加一个@Compose注解就可以将函数转化成Compose函数,同时Compose函数也只能在Compose函数中运行。这看起来似乎跟协程比较像,@Compose是不是也像协程一样,往函数中添加了一些参数呢?

我们就一起来看下,@Compose到底做了什么,又是怎么做到的。

/   前置知识   /

一看到@Compose注解,我们很容易就想到注解处理器,但是@Compose的解析并不是通过注解处理器来实现的,因为注解处理器只能生成代码,不能修改代码。而KCP(Kotlin Compiler Plugin):即kotlin编译插件,支持跨平台,android开发可以将它类比为kapt+transform机制,既可以生成代码,也可以修改代码。

@Compose注解的解析就是通过KCP来实现的。

什么是KCP

Kotlin编译过程简单来说,就是将Kotlin源码编译成字节码的过程,具体步骤如下所示:

d322d91f32ae9a1fb801ab3b8242b2eb.png

而Kotlin编译期插件则在编译过程中提供Hook时机,让我们可以解析符号,修改字节码生成结果等。

Kotlin库中的不少语法糖都用到了KCP,比如Kotlin-android-extension,@Parcelize等,@Compose注解同样也是通过KCP解析的。

相比KAPT,KCP主要有以下优点:

  1. KAPT是基于注解处理器的,它需要将Kotlin代码转化成Stub再解析注解生成代码,常常转化成Stub的时间比生成代码还要长,而KCP则是直接解析Kotlin的符号,因此在编译速度上KCP比KAPT要强的多

  2. KAPT只能生成代码,不能修改代码,而KCP不仅可以生成代码,也可以修改代码,可以看作是kapt+transorm机制

而KCP的缺点则在于,KCP 的开发成本太高,涉及 Gradle Plugin、Kotlin Plugin 等的使用,API 涉及一些编译器知识的了解,一般开发者很难掌握。因此如果只是需要处理注解生成代码,不需要修改代码,通常使用KSP就足够了,KSP是对KCP的一个封装。

KCP的基本概念

上面也说到了,KCP的开发成本较高,主要包括以下内容:

c8b48db68c6fe26aadca8d81066b2ac2.png

  • Plugin:Gradle 插件用来读取 Gradle 配置传递给 KCP(Kotlin Plugin)

  • Subplugin:为 KCP 提供自定义 KP 的 maven 库地址等配置信息

  • CommandLineProcessor:负责将Plugin传过来的参数转换并校验

  • ComponentRegistrar:负责将用户自定义的各种Extension注册到KP中,并在合适时机调用

ComponentRegistrar是核心入口,所有的KCP自定义功能都需要通过这个类注册一些Extension接口来实现。

下面列举一些常用的Extension接口,大家可以根据需求选用: 

  • IrGenerationExtension,用于增/删/改/查代码

  • DiagnosticSuppressor,用于抑制语法错误,Jetpack Compose有使用 

  • StorageComponentContainerContributor,用于实现IOC

/   @Compose注解的作用   /

上面介绍了KCP的基本概念,下面看下在Jetpack Compose中@Compose注解到底是怎么解析的,又起了什么作用。

注册IrGenerationExtension

上面我们介绍了ComponentRegistrar是核心入口,负责将用户自定义的各种Extension注册到KP中,并在合适时机调用,而IrGenerationExtension可以用于修改代码。

Compose插件的入口为ComposePlugin,其中也包括一个ComposeComponentRegistrar,IrGenerationExtension的注册就是在这里完成的。

class ComposeComponentRegistrar : ComponentRegistrar {
    override fun registerProjectComponents(
        project: MockProject,
        configuration: CompilerConfiguration
    ) {
        registerProjectExtensions(
            project as Project,
            configuration
        )
    }

    fun registerProjectExtensions(
            project: Project,
            configuration: CompilerConfiguration
    ) {
        IrGenerationExtension.registerExtension(
            project,
            ComposeIrGenerationExtension(
              //...
            )
        )
    }
}

如上所示,注册了IrGenerationExtension,接下来IrGenerationExtension会调用ComposerParamTransformer的相关方法,完成参数的填充,后续的主要处理工作都是在ComposerParamTransformer中处理了。

添加$Composer

上文说到,后续在函数中添加参数的工作主要是在ComposerParamTransformer中完成的,具体调用了IrFunction.withComposerParamIfNeeded。

private fun IrFunction.withComposerParamIfNeeded(): IrFunction {
    // 如果不是`Compose`函数,则直接返回,后续不再处理
    if (!this.hasComposableAnnotation()) {
        return this
    }

    // 如果此函数是作为参数的`Lambda`,并且不是`Compose`函数,则直接返回
    if (isNonComposableInlinedLambda()) return this

    // 不处理expect函数
    if (isExpect) return this

    // 缓存转换的结果
    return transformedFunctions[this] ?: copyWithComposerParam()
}

如上所示,主要就是判断一下函数是否有@Compose注解,如果没有则直接返回不再处理,有则继续处理并缓存结果,后续调用copyWithComposerParam方法。

private fun IrFunction.copyWithComposerParam(): IrSimpleFunction {
    //...

    return copy().also { fn ->
        // $composer
        val composerParam = fn.addValueParameter {
        name = KtxNameConventions.COMPOSER_PARAMETER
        type = composerType.makeNullable()
        origin = IrDeclarationOrigin.DEFINED
        isAssignable = true
        }

    //...
    }
}

如上所示,在所有Compose函数中插入了一个$composer,这有效地使Composer可用于任何子树,提供实现Composable树并保持更新所需的所有信息。

b23ac444621d0f7962537ef40ef2bd35.png

添加$changed

我们知道Compose存在智能重组机制,当输入完全相同时允许跳过重组,而编译器除了$composer,还会注入$changed 参数。

此参数用于提供有关当前 Composable 的输入参数与一次发生组件后是否相同,如果相同则允许跳过重组。

enum class ParamState(val bits: Int) {
    Uncertain(0b000),
    Same(0b001),
    Different(0b010),
    Static(0b011),
    Unknown(0b100),
    Mask(0b111);
}

如上所示,$changed通过位运算的方式来表示参数是否发生变化:

  1. $changed是Int类型,一个占32位

  2. 每个参数有5种类型,因此一个参数需要3位来表示

  3. 因此一个$changed可以表示10个参数是否发生变化,如果超出则需要再添加一个$changed参数

编译器注入$changed之后效果如下所示:

@Composable
fun A(x: Int, $composer: Composer<*>, $changed: Int) {
    var $dirty = $changed
    if ($changed and 0b0110 === 0) {
        $dirty = $dirty or if ($composer.changed(x)) 0b0010 else 0b0100
    }
    if (%dirty and 0b1011 !== 0b1010 || !$composer.skipping) {
        f(x)
    } else {
        $composer.skipToGroupEnd()
    }
}

添加$default

Kotlin 支持的默认参数不适用于可组合函数的参数,因为可组合函数需要在函数的作用域(生成的组)内为其参数执行默认表达式。

为此,Compose 提供了默认参数解析机制的替代实现。即在Compose方法中添加$defaulut。

private fun IrFunction.copyWithComposerParam(): IrSimpleFunction {
    //...

    // $default[n]
    if (oldFn.requiresDefaultParameter()) {
        val defaults = KtxNameConventions.DEFAULT_PARAMETER.identifier
        for (i in 0 until defaultParamCount(realParams)) {
            fn.addValueParameter(
            if (i == 0) defaults else "$defaults$i",
            context.irBuiltIns.intType,
            IrDeclarationOrigin.MASK_FOR_DEFAULT_FUNCTION
            )
       }
   }            

//...
}

$default与$changed类似,也是通过位运算来表示参数状态的,不过$default比较简单,只有两种状态,使用还是不使用默认值,因此一个$changed参数可表示31个参数是否使用默认值,如果超出再添加一个$changed编译器注入$default后的效果如下所示:

@Composable
fun A(x: Int, $default: Int) {
    val x = if ($default and 0b1 != 0) 0 else x
    f(x)
}

/   总结   /

本文主要简单介绍了什么是KCP及KCP是如何处理@Compose注解的,从中可以看到KCP的强大与复杂,如果你只需要解析注解生成代码的话,可以使用KSP取代KAPT,如果有更多需求,可以尝试使用KCP。

同时也可以看到Compose设计的巧妙,将框架背后的复杂度完全隐藏,背后做了这么多工作,使用都却只需添加一个@Compose注解,就能将一个普通的函数变成Compose函数,的确是挺简洁优雅的,感兴趣的同学也可以直接查看源码~

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

Compose 的 State 状态到底是个啥?

浅析Jetpack Compose是如何安装到View视图上

欢迎关注我的公众号

学习技术或投稿

2881bc75765c24b922c3fb4ae29baa72.png

bf47e78f1b9316943e79aa8bb2cc3505.png

长按上图,识别图中二维码即可关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值