企业网站建设600元,商城网站建设开发,搭建什么样的平台,苏州网站建设制作设计前言
Compose 的语法简洁、代码效率非常高#xff0c;这主要得益于 Compose Compiler 的一系列编译期魔法#xff0c;帮开发者生成了很多样板代码。但编译期插桩也阻碍了我们对于 Compose 运行原理的认知#xff0c;想要真正读懂 Compose 就必须先了解它的 Compiler。本系列…
前言
Compose 的语法简洁、代码效率非常高这主要得益于 Compose Compiler 的一系列编译期魔法帮开发者生成了很多样板代码。但编译期插桩也阻碍了我们对于 Compose 运行原理的认知想要真正读懂 Compose 就必须先了解它的 Compiler。本系列文章将带大家揭开 Compose Compiler 的神秘面纱。
Compose 是一个 Kotlin Only 框架所以 Compose Compiler 的本质是一个 KCPKotlin Compiler Plugin。在研究 Compose Compiler 源码之前先要铺垫一些 Kotlin Compiler 以及 KCP 的基础知识
Kotlin 编译流程
Kotlin 是一门跨平台语言Kotlin Compiler 可以将 Kt 源码编译成多个平台的目标代码JS、JVM 字节码甚至 LLVM 机器码。但无论编译成何种目标代码其编译过程都可以分为两个阶段
Frontend编译器前端对源代码分析得到 AST 抽象语法树以及符号表并完成静态检查Backend编译器后端基于 AST 等前端产物生成平台目标代码
简而言之前端负责源码的解析和检查后端负责目标代码的生成 如上以 Kotlin/JVM 为例
Frontend 处理中Kt 源文件经过词法、语法和语义分析LexerPaser生成 PSI 以及对应的 BindingContext。Backend 处理中基于 PSI 和 BindingContext 先生成 JVM 字节码然后通过 ASM 将字节码二进制化生成 class 文件
不同目标平台的编译流程中 Frontend 的处理流程都一样只是在 Backend 中生成不同的目标代码
K1 编译器PSI BindingContext
PSI 全称 Program Structure Interface 可以将它理解为 JetBrains 专用的 AST标准 AST 之上有一些扩展。PSI 可以用于编译过程中的语法静态检查PSI 也用于 IntelliJ 系列 IDE 的静态检查我们在编写代码过程中能实时提示语法错误就是靠它。因此 PSI 有助于编译和编写阶段复用静态检查逻辑。我们在开发 IDE Plugin 或者编写 Detekt 静态检查用例时都有机会使用到 PSI。 PSI: https://plugins.jetbrains.com/docs/intellij/psi-elements.htmlDetekt: https://github.com/detekt/detekt 在 IDE 中通过 PsiViewer 插件可以实时看到源码对应的 PSI以下面代码为例
fun main() {println(Hello, World!)
}上图是 PsiViewer 中的输出结果可以看到它体现了以下树形结构 PSI 树的节点是源码经分析后的语法元素例如一个特殊符号一个字符串等这都是一个个 PsiElement。PsiElement 仍然缺少了基于上下文的语义信息比如对于一个 KtFunction它的参数信息修饰符信息等等这就需要 BindingContext 的辅助了。
BindingContext 相当于 PSI 配套的符号表PsiElement 经语义分析后得到对应的 Descriptor 描述符并记录到 BindingContext 中BindingContext 可以快速索引到 PSI 节点对应的 Descriptor。Descriptor 包含我们需要的语义信息例如 FunctionDescriptor 可以获取 TypeParametersisInline 等信息。 BindingContext 结构类似一个 MapType, Mapkey, Descriptor 第一个 Map 的 key 代表 PSI 节点类型第二个 Map 的 key 是 PsiElement 实例Value 是其对应的 Descriptor。KtFunction 为 key 可以获取对应的 FunctionDescriptorKtCallExpression 获取对应的 ResolvedCall这里面包含了调用方法的 FunctionDescriptor 以及传入的 Parameters。
K2 编译器FIR IR
通过上面的介绍我们知道Kotlin Compiler 的 Frotend 产物是 PSI 以及 BindingContextBackend 将基于它们直接输出目标代码。由于 Backend 耦合了目标代码生成逻辑一些编译期的处理和优化逻辑难以多平台复用。例如我们都知道的 suspend 函数在编译期会生成额外的代码而我们希望这些 codegen 逻辑得以复用为此 Kotlin 开发了新一代编译器取名为 K2 。 K2: https://blog.jetbrains.com/zh-hans/kotlin/2021/10/the-road-to-the-k2-compiler/ K2 编译器的最大特点是引入了 IRIntermediate Representation中间表达。IR 是连接前后端的中间产物 它与平台无关类似 suspend 这类编译期优化可以面向 IR 实现并跨平台复用。
K2 中使用新的基于 IR 的 Backend 替代旧有的基于 PSI 和 BindingContext 的 Backend。Kotlin 1.5 开始 Kotlin/JVM 默认启用新的 IR Backend1.6 开始 Kotin/JS IR Backend 成了标配。下图是引入 IR Backend 的编译流程。 IR 也是一颗树形数据结构但它的抽象表达更加“低级”更贴近 CPU 架构。IrElement 带有多种语义信息例如 FUN 的 visibilitymodality 以及 returnType 等等不必像 PsiElement 那样需要通过查询 BindingContext 获取这些信息。
前面 Hello World 的例子其对应的 IR 树打印如下
FUN name:main visibility:public modality:FINAL () returnType:kotlin.UnitBLOCK_BODYCALL public final fun println (message: kotlin.Any?): kotlin.Unit [inline] declared in kotlin.io.ConsoleKt typekotlin.Unit originnullmessage: CONST String typekotlin.String valueHello, World!除了新的 IR BackendK2 也更新了 Frontend主要变化是使用 FIR Frontend IR替代了 PSI 与 BindingContext。1.7.0 起我们可以使用到 K2 的新前端。 综上可见: K2 相对于 K1 的主要变化引入了 FIR Frontend 和 IR Backend。
IR 可以由 FIR 转化而来它们都是树型结构那么这两者又有什么区别呢可以从以下三个方面进行区分
FIRIR目标不同FIR 整合了 PSI 与 BindingContext 信息更快速地查找描述符信息它的首要目标是提升前端静态分析以及检查的性能性能不是 IR 的考虑它的数据结构的出发点不是为了提升后端编译速度而是服务于不同后端之间的编译逻辑共享降低不同平台支持新语言特性的成本结构不同FIR 仍然是一颗 AST只是增强了一些符号信息加速静态分析IR 不仅是一颗 AST它提供了更丰富的基于上下文的语义信息比如我可以知道某个代码块中的某个变量是临时变量还是成员变量而 FIR 难以做到能力不同虽然 FIR 也可以处理一些简单的脱糖和代码生成工作但整体上仍然是服务于前端不能对 AST 大幅度修改IR 具有丰富的 Godegen API可以更加灵活地对树形结构进行 add/remove/update实现任意编译期的魔改需求
KCPKotlin Compiler Plugin
KCP 允许我们在上述 Kotlin 编译过程中通过增加扩展点以实现各种编译期魔改。Kotlin 的不少语法糖都是基于 KCP 实现的比如大家熟知的 No-arg、All-open、kotlinx-serialization 等等。
KCP 也可以像 KAPT 那样在编译期进行注解处理但它相对于 KATP 更具优势 KCP 在 Kotlin 编译过程中进行而 KAPT 需要在正式编译之前增加额外的预编译环节因此 KCP 的性能更好。KSPKotlin Symbol Processing也是基于 KCP 实现的这也是为什么 KSP 的性能更好的原因 KAPT 主要是用来生成新代码难以针对原有代码逻辑做修改。KCP 可以针对 Bytecode 或者 IR 做任意修改能力更强大。
KCP 的开发步骤
KCP 虽然功能强大但是开发难度较高开发一个完整的 KCP 要涉及多个步骤 Gradle Plugin PluginKCP 是通过 Gradle 配置的需要定义一个 Gradle 插件并在 Gradle 中配置 KCP 所需的编译参数。Subplugin 建立从 Gradle Plugin 到 Kotlin Plugin 的连接并将 Gradle 中配置的参数传递给 Kotlin Plugin Kotlin Plugin CommandLineProcessorKCP 的入口定义 KCP 的 id、解析命令行参数等ComponentRegister注册 KCP 中的 Extension 扩展点。它与 CommandLineProcessor 一样都是通过 SPI 调用需要添加 auto-service 注解XXExtension这是实现 KCP 逻辑的地方。Kotlin 提供了许多类型的 Extension 供我们实现。编译器会在前端、后端的各个编译环节中调用 KCP 注册的对应类型的 Extension。例如 ExpressionCodegenExtension 可用来修改 Class 的 BodyClassBuilderInterceptorExtension 可以修改 Class 的 Definition 等等
随着 Kotlin Compiler 从 K1 升级到 K2KCP 也提供了面向 K2 的 Extension。
以 No-arg 为例 No-arg 通过为 Class 添加注解自动生成无参构造函数。No-arg 源码中存在 K1、K2 两套 Extension可以兼容不同 Kotlin 版本的使用 No-arg: https://kotlinlang.org/docs/no-arg-plugin.htmlsourcehttps://cs.android.com/android-studio/kotlin//master:plugins/noarg/ NoArg K1 CliNoArgDeclarationCheckerNoArg 不能作用于 Inner Class这里使用基于 PSI 的前端检查逻辑检查是否是 Inner ClassCliNoArgExpressionCodegenExtension继承自 ExpressionCodegenExtension基于 PSI 和对应的 Descriptor 以 JVM 字节码的形式在 Class Body 中添加无参构造函数 NoArg K2: FirNoArgDeclarationChecker新的 K2 前端可基于 FIR 检查 InnerClassNoArgIrGenerationExtension继承自 IrGenerationExtension 基于 IR 添加无参构造函数
以 Backend Extension 为例体会以下具体实现上的区别
CliNoArgExpressionCodegenExtension 中的处理
// 1. 基于 descriptor 获取 class 信息
val superClassInternalName typeMapper.mapClass(descriptor.getSuperClassOrAny()).internalName
val constructorDescriptor createNoArgConstructorDescriptor(descriptor)
val superClass descriptor.getSuperClassOrAny()// 2. 通过 Codegen 直接生成无参构造函数对应的字节码
functionCodegen.generateMethod(JvmDeclarationOrigin.NO_ORIGIN, constructorDescriptor, object : CodegenBased(state) {override fun doGenerateBody(codegen: ExpressionCodegen, signature: JvmMethodSignature) {codegen.v.load(0, AsmTypes.OBJECT_TYPE)if (isParentASealedClassWithDefaultConstructor) {codegen.v.aconst(null)codegen.v.visitMethodInsn(Opcodes.INVOKESPECIAL, superClassInternalName, init,(Lkotlin/jvm/internal/DefaultConstructorMarker;)V, false)} else {codegen.v.visitMethodInsn(Opcodes.INVOKESPECIAL, superClassInternalName, init, ()V, false)}if (invokeInitializers) {generateInitializers(codegen)}codegen.v.visitInsn(Opcodes.RETURN)}
})NoArgIrGenerationExtension 中的处理
// 1. 基于 IrClass 获取 Class 信息
val superClass klass.superTypes.mapNotNull(IrType::getClass).singleOrNull { it.kind ClassKind.CLASS }?: context.irBuiltIns.anyClass.owner
val superConstructor if (needsNoargConstructor(superClass))getOrGenerateNoArgConstructor(superClass)else superClass.constructors.singleOrNull { it.isZeroParameterConstructor() }?: error(No noarg super constructor for ${klass.render()}:\n superClass.constructors.joinToString(\n) { it.render() })// 2. 基于 irFactory 等 IR API 创建构造函数
context.irFactory.buildConstructor {startOffset SYNTHETIC_OFFSETendOffset SYNTHETIC_OFFSETreturnType klass.defaultType
}.also { ctor -ctor.parent klassctor.body context.irFactory.createBlockBody(ctor.startOffset, ctor.endOffset,listOfNotNull(IrDelegatingConstructorCallImpl(ctor.startOffset, ctor.endOffset, context.irBuiltIns.unitType,superConstructor.symbol, 0, superConstructor.valueParameters.size),IrInstanceInitializerCallImpl(ctor.startOffset, ctor.endOffset, klass.symbol, context.irBuiltIns.unitType).takeIf { invokeInitializers }))
}NoArgIrGenerationExtension 是一个 IrGenerationExtension这是专门用来更新 Ir 的扩展点可以看到里面已经没有了对字节码的操作取而代之使用 IR 中的各种 buildXXX API。
Compose Compiler 的代码生成也是依靠 IrGenerationExtension 实现的所以即使最早版本的 Compose 也要求 Kotlin 版本大于 1.5.10就是因其 Compiler 只支持 IR Backend Extension。
Compose Compiler
Compose Compiler 本质上是一个 KCP在了解了 KCP 的基本构成之后我们知道 Compose Compiler 的核心在于 Extension Compose Compiler: https://cs.android.com/androidx/platform/frameworks/support//androidx-main:compose/compiler/compiler-hosted/ 直接找到 ComposeComponentRegistrar查看注册了哪些 Extension
class ComposeComponentRegistrar : ComponentRegistrar {//...StorageComponentContainerContributor.registerExtensioproject,ComposableCallChecker())StorageComponentContainerContributor.registerExtensioproject,ComposableDeclarationChecker())StorageComponentContainerContributor.registerExtensioproject,ComposableTargetChecker())ComposeDiagnosticSuppressor.registerExtension(project,ComposeDiagnosticSuppressor())Suppress(OPT_IN_USAGE_ERROR)TypeResolutionInterceptor.registerExtension(project,Suppress(IllegalExperimentalApiUsage)ComposeTypeResolutionInterceptorExtension())IrGenerationExtension.registerExtension(project,ComposeIrGenerationExtension(configuration configuration,liveLiteralsEnabled liveLiteralsEnabled,liveLiteralsV2Enabled liveLiteralsV2EnabledgenerateFunctionKeyMetaClasses generateFuncsourceInformationEnabled sourceInformationEintrinsicRememberEnabled intrinsicRememberEdecoysEnabled decoysEnabled,metricsDestination metricsDestination,reportsDestination reportsDestination,))DescriptorSerializerPlugin.registerExtension(project,ClassStabilityFieldSerializationPlugin())//...
}ComposableCallChecker检查是否可以调用 Composable 函数ComposableDeclarationChecker检查 Composable 的位置是否正确ComposeDiagnosticSuppressor屏蔽不必要的编译诊断错误ComposeIrGenerationExtension负责 Composable 函数的代码生成ClassStabilityFieldSerializationPlugin分析 Class 是否稳定并添加稳定性信息
这里的各种 Checker 是 Frontend Extension 目前仍然是基于 K1 实现的而位于 Backend 的 ComposeIrGenerationExtension 则面向 K2这也是 Compose 代码生成的核心会在本系列的后续文章中重点介绍。
参考 Writing Your First Kotlin Compiler Plugin https://resources.jetbrains.com/storage/products/kotlinconf2018/slides/5_Writing%20Your%20First%20Kotlin%20Compiler%20Plugin.pdf Kotlin Compiler Internals In 1.4 and beyond https://docs.google.com/presentation/d/e/2PACX-1vTzajwYJfmUi_Nn2nJBULi9bszNmjbO3c8K8dHRnK7vgz3AELunB6J7sfBodC2sKoaKAHibgEt_XjaQ/pub?slideid.g955e8c1462_0_190