乐趣区

关于android:告别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 架构如下所示。


上图中波及到几个具体的概念:

  • 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,构造如下图。

能够看到,一个 Kotlin 源文件经 KSP 解析后失去的 Kotlin AST 如下所示。

KSFile
  packageName: KSName
  fileName: String
  annotations: List<KSAnnotation>  (File annotations)
  declarations: List<KSDeclaration>
    KSClassDeclaration // class, interface, object
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      classKind: ClassKind
      primaryConstructor: KSFunctionDeclaration
      superTypes: List<KSTypeReference>
      // contains inner classes, member functions, properties, etc.
      declarations: List<KSDeclaration>
    KSFunctionDeclaration // top level function
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      functionKind: FunctionKind
      extensionReceiver: KSTypeReference?
      returnType: KSTypeReference
      parameters: List<KSValueParameter>
      // contains local classes, local functions, local variables, etc.
      declarations: List<KSDeclaration>
    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<KSAnnotated> // 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<String>()
    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 工程。

而后,在根我的项目中指定 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
退出移动版