告别KAPT,使用KSP为Android编译提速

一、KSP

在进行Android应用开发时,不少人吐槽 Kotlin 的编译速度慢,而KAPT 便是拖慢编译的元凶之一。我们知道,Android的很多库都会使用注解简化模板代码,例如 Room、Dagger、Retrofit 等,而默认情况下Kotlin 使用的是 KAPT 来处理注解的。KAPT没有专门的注解处理器,需要借助APT实现的,因此需要先生成 APT 可解析的 stub (Java代码),这拖慢了 Kotlin 的整体编译速度。

KSP 正是在这个背景下诞生的,它基于 Kotlin Compiler Plugin(简称KCP) 实现,不需要生成额外的 stub,编译速度是 KAPT 的 2 倍以上。除了 大幅提高 Kotlin 开发者的构建速度,该工具还提供了对 Kotlin/Native 和 Kotlin/JS 的支持。

二、KSP 与 KCP

这里提到了Kotlin Compiler Plugin ,KCP是在 kotlinc 过程中提供 Hook 时机,可以在期间解析 AST、修改字节码产物等,Kotlin 的不少语法糖都是 KCP 实现的。例如, data class、 @Parcelize、kotlin-android-extension 等,如今火爆的 Jetpack Compose也是借助 KCP 完成的。

理论上来说, KCP 的能力是 KAPT 的超集,完全可以替代 KAPT 以提升编译速度。但是 KCP 的开发成本太高,涉及 Gradle Plugin、Kotlin Plugin 等的使用,API 涉及一些编译器知识的了解,一般开发者很难掌握。一个标准 KCP 架构如下所示。

告别KAPT,使用KSP为Android编译提速_第1张图片
上图中涉及到几个具体的概念:

  • Plugin:Gradle 插件用来读取 Gradle 配置传递给 KCP(Kotlin Plugin);
  • Subplugin:为 KCP 提供自定义 Kotlin Plugin 的 maven 库地址等配置信息;
  • CommandLineProcessor:将参数转换为 Kotlin Plugin 可识别参数;
  • ComponentRegistrar:注册 Extension 到 KCP 的不同流程中;
  • Extension:实现自定义的Kotlin Plugin功能;

KSP 简化了KCP的整个流程,开发者无需了解编译器工作原理,处理注解等成本也变得像 KAPT 一样低。

三、KSP 与 KAPT

KSP 顾名思义,在 Symbols 级别对 Kotlin 的 AST 进行处理,访问类、类成员、函数、相关参数等类型的元素。可以类比 PSI 中的 Kotlin AST,结构如下图。

告别KAPT,使用KSP为Android编译提速_第2张图片

可以看到,一个 Kotlin 源文件经 KSP 解析后得到的 Kotlin AST如下所示。

KSFile
  packageName: KSName
  fileName: String
  annotations: List  (File annotations)
  declarations: List
    KSClassDeclaration // class, interface, object
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      classKind: ClassKind
      primaryConstructor: KSFunctionDeclaration
      superTypes: List
      // contains inner classes, member functions, properties, etc.
      declarations: List
    KSFunctionDeclaration // top level function
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      functionKind: FunctionKind
      extensionReceiver: KSTypeReference?
      returnType: KSTypeReference
      parameters: List
      // contains local classes, local functions, local variables, etc.
      declarations: List
    KSPropertyDeclaration // global variable
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      extensionReceiver: KSTypeReference?
      type: KSTypeReference
      getter: KSPropertyGetter
        returnType: KSTypeReference
      setter: KSPropertySetter
        parameter: KSValueParameter

类似的, APT/KAPT 则是对 Java AST 的抽象,我们可以找到一些对应关系,比如 Java 使用 Element 描述包、类、方法或者变量等, KSP 中使用 Declaration。

Java/APT Kotlin/KSP 描述
PackageElement KSFile 一个包程序元素,提供对有关包及其成员的信息的访问
ExecuteableElement KSFunctionDeclaration 某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素
TypeElement KSClassDeclaration 一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口
VariableElement KSVariableParameter / KSPropertyDeclaration 一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数

Declaration 之下还有 Type 信息 ,比如函数的参数、返回值类型等,在 APT 中使用 TypeMirror 承载类型信息 ,KSP 中详细的能力由 KSType 实现。

四、KSP 入口SymbolProcessorProvider

KSP的入口在SymbolProcessorProvider ,代码如下:

interface SymbolProcessorProvider {
    fun create(environment: SymbolProcessorEnvironment): SymbolProcessor
}

SymbolProcessorEnvironment 主要用于获取 KSP 运行时的依赖,注入到 Processor:

interface SymbolProcessor {
    fun process(resolver: Resolver): List // Let's focus on this
    fun finish() {}
    fun onError() {}
}

process() 方法需要提供一个 Resolver , 解析 AST 上的 symbols,Resolver 使用访问者模式去遍历 AST。如下,Resolver 使用 FindFunctionsVisitor 找出当前 KSFile 中 top-level 的 function 以及 Class 成员方法。

class HelloFunctionFinderProcessor : SymbolProcessor() {
    ...
    val functions = mutableListOf()
    val visitor = FindFunctionsVisitor()

    override fun process(resolver: Resolver) {
        resolver.getAllFiles().map { it.accept(visitor, Unit) }
    }

    inner class FindFunctionsVisitor : KSVisitorVoid() {
        override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
            classDeclaration.getDeclaredFunctions().map { it.accept(this, Unit) }
        }

        override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) {
            functions.add(function)
        }

        override fun visitFile(file: KSFile, data: Unit) {
            file.declarations.map { it.accept(this, Unit) }
        }
    }
    ...
    
    class Provider : SymbolProcessorProvider {
        override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = ...
    }
}

五、快速上手

5.1 创建processor

首先,创建一个空的gradle工程。
告别KAPT,使用KSP为Android编译提速_第3张图片
然后,在根项目中指定Kotlin插件的版本,以便在其他项目模块中使用,比如。

plugins {
    kotlin("jvm") version "1.6.0" apply false
}

buildscript {
    dependencies {
        classpath(kotlin("gradle-plugin", version = "1.6.0"))
    }
}

不过,为了统一项目中Kotlin的版本,可以在gradle.properties文件中进行统一的配置。

kotlin.code.style=official
kotlinVersion=1.6.0
kspVersion=1.6.0-1.0.2

接着,添加一个用于承载处理器的模块。并在模块的build.gradle.kts文件中添加如下脚步。

plugins {
    kotlin("jvm")
}

repositories {
    mavenCentral()
}

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

接着,我们需要实现com.google.devtools.ksp.processing.SymbolProcessor和com.google.devtools.ksp.processing.SymbolProcessorProvider。SymbolProcessorProvider的实现作为一个服务加载,以实例化实现的SymbolProcessor。使用时需要注意以下几点:

  • 使用SymbolProcessorProvider.create()来创建一个SymbolProcessor。processor需要的依赖则可以通过SymbolProcessorProvider.create()提供的参数进行传递。
  • 主要逻辑应该在SymbolProcessor.process()方法中执行。
  • 使用resoler.getsymbolswithannotation()来获得我们想要处理的内容,前提是给出注释的完全限定名称,比如com.example.annotation.Builder
  • KSP的一个常见用例是实现一个定制的访问器,接口com.google.devtools.ksp.symbol.KSVisitor,用于操作符号。
  • 有关SymbolProcessorProvider和SymbolProcessor接口的使用示例,请参见示例项目中的以下文件:src/main/kotlin/BuilderProcessor.ktsrc/main/kotlin/TestProcessor.kt
  • 编写自己的处理器之后,在resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider中包含处理器提供商的完全限定名,将其注册到包中。

5.2 使用processor

5.2.1 使用Kotlin DSL

再创建一个模块,包含处理器需要处理的工作。然后,在build.gradle.kts文件中添加如下代码。

pluginManagement {
    repositories {
       gradlePluginPortal()
    }
}

在新模块的build.gradle中,我们主要完成以下事情:

  • 应用带有指定版本的com.google.devtools.ksp插件。
  • 将ksp添加到依赖项列表中

比如:

plugins {
        id("com.google.devtools.ksp") version kspVersion
        kotlin("jvm") version kotlinVersion
    }

运行./gradlew命令进行构建,可以在build/generated/source/ksp下找到生成的代码。下面是一个build.gradle.kts将KSP插件应用到workload的示例。

plugins {
    id("com.google.devtools.ksp") version "1.6.0-1.0.2"
    kotlin("jvm") 
}

version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation(kotlin("stdlib-jdk8"))
    implementation(project(":test-processor"))
    ksp(project(":test-processor"))
}

5.2.2 使用Groovy

在您的项目中构建。Gradle文件添加了一个包含KSP插件的插件块

plugins {
  id "com.google.devtools.ksp" version "1.5.31-1.0.0"
}

然后,在dependencies添加如下依赖。

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation project(":test-processor")
    ksp project(":test-processor")
}

SymbolProcessorEnvironment提供了processors选项,选项在gradle构建脚本中指定。

  ksp {
    arg("option1", "value1")
    arg("option2", "value2")
    ...
  }

5.3 使用IDE生成代码

默认情况下,IntelliJ或其他ide是不知道生成的代码的,因此对这些生成符号的引用将被标记为不可解析的。为了让IntelliJ能够对生成的代码进行操作,需要添加如下配置。

build/generated/ksp/main/kotlin/
build/generated/ksp/main/java/

当然,也可以是资源目录。

build/generated/ksp/main/resources/

在使用的时候,还需要在KSP processor模块中配置这些生成的目录。

kotlin {
    sourceSets.main {
        kotlin.srcDir("build/generated/ksp/main/kotlin")
    }
    sourceSets.test {
        kotlin.srcDir("build/generated/ksp/test/kotlin")
    }
}

如果在Gradle插件中使用IntelliJ IDEA和KSP,那么上面的代码片段会给出以下警告:

Execution optimizations have been disabled for task ':publishPluginJar' to ensure correctness due to the following reasons:

对于这种警告,我们可以在模块中添加下面的代码。

plugins {
    // …
    idea
}
// …
idea {
    module {
        // Not using += due to https://github.com/gradle/gradle/issues/8749
        sourceDirs = sourceDirs + file("build/generated/ksp/main/kotlin") // or tasks["kspKotlin"].destination
        testSourceDirs = testSourceDirs + file("build/generated/ksp/test/kotlin")
        generatedSourceDirs = generatedSourceDirs + file("build/generated/ksp/main/kotlin") + file("build/generated/ksp/test/kotlin")
    }
}

目前,已有不少使用 APT 的三方库增加了对 KSP 的支持,如下。

Library Status Tracking issue for KSP
Room Experimentally supported
Moshi Officially supported
RxHttp Officially supported
Kotshi Officially supported
Lyricist Officially supported
Lich SavedState Officially supported
gRPC Dekorator Officially supported
Auto Factory Not yet supported Link
Dagger Not yet supported Link
Hilt Not yet supported Link
Glide Not yet supported Link
DeeplinkDispatch Supported via airbnb/DeepLinkDispatch#323

你可能感兴趣的