Administrator
发布于 2026-08-25 / 29 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 51 篇:Kotlin 编译器插件:从入门到写一个 @Composable

上一篇聊了代码质量保障,这篇钻进 Kotlin 编译器插件。Compose 的 @Composable、KSP 的 @Inject、Room 的 @Dao 都是编译器插件的产物。理解它,才能从"调 API"升级到"造 API"。

什么是 Kotlin 编译器插件

Kotlin 编译器分 Frontend(解析、类型检查)和 Backend(生成字节码)两段,插件可以介入多个阶段:

阶段

能做什么

FIR(Frontend IR)

读取源码、改类型、注册新语法

IR(Intermediate Representation)

改函数体、加新方法

JVM Backend

在字节码生成阶段插入逻辑

@Composable 的实现就是 IR 阶段:编译器把函数体包一层 Composer.startReplaceableGroup(...),跟踪调用顺序。

用 KSP 还是编译器插件

新同学分不清这两个工具:

KSP(Kotlin Symbol Processing):基于 Kotlin 语法的轻量级注解处理器。简单、快、能访问 AST,但改不了函数体。常用于生成代码(Room、Glide、Hilt 部分功能)。

Kotlin Compiler Plugin:完整的编译器扩展。能改 IR,能加新语法(如 @Composable)。复杂、慢、版本敏感,但无所不能。

选 KSP 除非你真的需要改函数体。

写一个最简 KSP 处理器

环境准备:

// build.gradle.kts (project)
plugins {
    id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
}

// build.gradle.kts (module)
plugins {
    id("com.google.devtools.ksp")
    kotlin("jvm") // 或 android
}

dependencies {
    implementation("com.google.devtools.ksp:com.google.devtools.ksp.symbol-processing-api:2.0.21-1.0.27")
}

Processor 代码:

// src/main/kotlin/com/example/builder/BuilderProcessor.kt
package com.example.builder

import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*
import com.google.devtools.ksp.validate.*

class BuilderProcessor(
    environment: SymbolProcessorEnvironment
) : SymbolProcessor {

    private val logger = environment.logger

    override fun process(resolver: Resolver): List<KSAnnotated> {
        // 找出所有 @Builder 注解的类
        val symbols = resolver.getSymbolsWithAnnotation("com.example.Builder")

        symbols.forEach { ksAnnotated ->
            when (ksAnnotated) {
                is KSClassDeclaration -> generateBuilder(ksAnnotated)
            }
        }

        // 返回未处理的符号,让其他 processor 处理
        return emptyList()
    }

    private fun generateBuilder(cls: KSClassDeclaration) {
        val packageName = cls.packageName.asString()
        val className = cls.simpleName.asString()
        val builderName = "${className}Builder"

        val fields = cls.primaryConstructor?.parameters ?: return

        val fieldDeclarations = fields.joinToString("\n") { param ->
            "    private var ${param.name?.asString()}: ${param.type.resolve().declaration.qualifiedName?.asString()}? = null"
        }

        val setters = fields.joinToString("\n") { param ->
            val name = param.name?.asString()
            val type = param.type.resolve().declaration.qualifiedName?.asString()
            "    fun $name(value: $type) = apply { this.$name = value }"
        }

        val params = fields.joinToString(", ") { it.name?.asString() }
        val assigns = fields.joinToString("\n        ") {
            "        ${it.name?.asString()} = ${it.name?.asString()}!!"
        }

        val code = """
            package $packageName

            class $builderName {
            $fieldDeclarations
            $setters

                fun build(): $className = $className(
                    $assigns
                )
            }
        """.trimIndent()

        // 写到 generated sources 目录
        // (实际用 environment.codeGenerator.createNewFile(...))
        logger.info("Generated $builderName")
    }
}

// ProcessorProvider 注册
class BuilderProcessorProvider : SymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor =
        BuilderProcessor(environment)
}

注册:

// src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
com.example.BuilderProcessorProvider

使用:

@Builder
data class User(
    val name: String,
    val age: Int
)

// 生成 UserBuilder
val user = UserBuilder()
    .name("Alice")
    .age(30)
    .build()

输出文件而不是打印日志

实际项目要把生成的代码写到文件:

private fun generateBuilder(cls: KSClassDeclaration) {
    val codeGenerator = environment.codeGenerator // 需要传进来

    val file = codeGenerator.createNewFile(
        dependencies = Dependencies(false, cls.containingFile!!),
        packageName = cls.packageName.asString(),
        fileName = "${cls.simpleName.asString()}Builder"
    )

    file.bufferedWriter().use { writer ->
        writer.write("""
            package ${cls.packageName.asString()}

            class ${cls.simpleName.asString()}Builder {
            ${fields.joinToString("\n") { ... }}
            }
        """.trimIndent())
    }
}

调试 KSP

KSP 默认不输出到控制台,加参数看日志:

./gradlew :app:kspDebugKotlin --info

或者通过 Logger 输出:

environment.logger.warn("Generated: $className")

IntelliJ 中断点调试 KSP:

// processor 中加
if (System.getProperty("ksp.debug") == "true") {
    Thread.sleep(10000) // 等待 10 秒,趁机 attach
}

用 jstack 找到 KSP 进程,用 IDE 的 Attach to Process 功能连上去设断点。

Kotlin Compiler Plugin 的真相

KSP 写处理器很简单,但 Compose 的 @Composable 必须在编译器 IR 阶段改函数体。这要用 Kotlin Compiler Plugin。

插件结构:

my-plugin/
├── gradle.properties
├── build.gradle.kts
└── src/main/kotlin/
    └── com/example/myplugin/
        ├── MyPlugin.kt           // CompilerPluginRegistrar
        ├── MyIrGenerationExtension.kt  // 改 IR
        └── components/
            └── MyIrTransformer.kt  // 实际转换

CompilerPluginRegistrar:

package com.example.myplugin

import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.compiler.plugin.*

@CompilerPluginRegistrar(
    version = "1.0.0",
    componentStorageComponents = []
)
class MyPluginRegistrar : CompilerPluginRegistrar() {
    override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) {
        IrGenerationExtension.registerExtension(MyIrExtension())
    }
}

IrGenerationExtension 是真正改函数体的地方,需要遍历 IrModuleFragment:

class MyIrExtension : IrGenerationExtension {
    override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
        moduleFragment.accept(IrTransformer(pluginContext), null)
    }
}

写这一步要懂 IR API(IrFunction、IrCall、IrExpressionBody 等),通常参考已有的开源项目(kotlin-publish-subscribe-plugin 等)。

实战建议

  1. 新需求先看 KSP:90% 的注解处理器用 KSP 足够,调试简单

  2. 慎用 compiler plugin:跟 Kotlin 版本强绑定,每次升级 Kotlin 都要重新适配

  3. 测试用 kspTest:KSP 提供 KSPTestRunner 用 JVM 跑测试

  4. 文档和示例少:Compiler Plugin 文档稀缺,主要靠看 Compose 的源码学习

小结

KSP 是日常 Android 开发的实用工具,能写自定义注解处理器、自动生成样板代码(Builder、Factory、Mapper)。Compiler Plugin 是登堂入室级别的工具,需要懂 IR、依赖底层 API,门槛很高但能做 Compose 级别的魔法。建议先用 KSP 解决实际问题,需要时再深入 compiler plugin。

下一篇聊聊 ASM 字节码插桩,从 KSP 的"写文件"升级到"改字节码"。


评论