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

·  阅读 3123
@Compose 注解到底做了什么?了解一下~

前言

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

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

前置知识

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

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

什么是KCP

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

Kotlin编译期插件则在编译过程中提供Hook时机,让我们可以解析符号,修改字节码生成结果等。
Kotlin库中的不少语法糖都用到了KCP,比如Kotlin-android-extension@Parcelize等,@Compose注解同样也是通过KCP解析的

相比KAPTKCP主要有以下优点:

  1. KAPT是基于注解处理器的,它需要将Kotlin代码转化成Stub再解析注解生成代码,常常转化成Stub的时间比生成代码还要长,而KCP则是直接解析Kotlin的符号,因此在编译速度上KCPKAPT要强的多
  2. KAPT只能生成代码,不能修改代码,而KCP不仅可以生成代码,也可以修改代码,可以看作是kapt+transorm机制

KCP的缺点则在于,KCP 的开发成本太高,涉及 Gradle PluginKotlin Plugin 等的使用,API 涉及一些编译器知识的了解,一般开发者很难掌握。
因此如果只是需要处理注解生成代码,不需要修改代码,通常使用KSP就足够了,KSP是对KCP的一个封装,如果对KSP的使用详情感兴趣可参见:告别KAPT!使用 KSP 为 Kotlin 编译提速

KCP的基本概念

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

  • PluginGradle 插件用来读取 Gradle 配置传递给 KCPKotlin Plugin
  • Subplugin:为 KCP 提供自定义 KPmaven 库地址等配置信息
  • 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,其中也包括一个ComposeComponentRegistrarIrGenerationExtension的注册就是在这里完成的

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树并保持更新所需的所有信息。

添加$changed

我们知道Compose存在智能重组机制,当输入完全相同时允许跳过重组,而编译器除了$composer,还会注入$changed 参数。 此参数用于提供有关当前 Composable 的输入参数与一次发生组件后是否相同,如果相同则允许跳过重组。

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

        return copy().also { fn ->
            // $changed[n]
            val changed = KtxNameConventions.CHANGED_PARAMETER.identifier
            //changedparamCount,计算$changed数量
            for (i in 0 until changedParamCount(realParams, fn.thisParamCount)) {
                fn.addValueParameter(
                    if (i == 0) changed else "$changed$i",
                    context.irBuiltIns.intType
                )
            }            

           //...
        }
    }
复制代码

如上所示,添加的是个$changed[n],这个n是从何而来呢?这是因为每个参数的状态有5种情况,compose中定义了一个枚举,如下所示:

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

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

  1. $changedInt类型,一个占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)
    }
复制代码

总结

本文主要简单介绍了什么是KCPKCP是如何处理@Compose注解的,从中可以看到KCP的强大与复杂,如果你只需要解析注解生成代码的话,可以使用KSP取代KAPT,如果有更多需求,可以尝试使用KCP
同时也可以看到Compose设计的巧妙,将框架背后的复杂度完全隐藏,背后做了这么多工作,使用都却只需添加一个@Compose注解,就能将一个普通的函数变成Compose函数,的确是挺简洁优雅的,感兴趣的同学也可以直接查看源码~

参考资料

告别KAPT!使用 KSP 为 Kotlin 编译提速
Kotlin 编译器插件:我们究竟在期待什么?

分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改