告別KAPT!使用 KSP 爲 Kotlin 編譯提速

本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!前端

今年初 Android 發佈了 Kotlin Symbol Processing(KSP)的首個 Alpha 版,幾個月過去,KSP 已經更新到 Beta3 了, 目前 API 已經基本穩定,相信距離穩定版發佈也不會很遠了。node

爲何使用 KSP ?

很多人吐槽 Kotlin 的編譯速度,KAPT 即是拖慢編譯的元兇之一。android

不少庫都會使用註解簡化模板代碼,例如 Room、Dagger、Retrofit 等,Kotlin 代碼使用 KAPT 處理註解。 KAPT 本質上是基於 APT 工做的,APT 只能處理 Java 註解,所以須要先生成 APT 可解析的 stub (Java代碼),這拖慢了 Kotlin 的總體編譯速度。git

KSP 正是在這個背景下誕生的,它基於 Kotlin Compiler Plugin(簡稱KCP) 實現,不須要生成額外的 stub,編譯速度是 KAPT 的 2 倍以上github

KSP 與 KCP

Kotlin Compiler Plugin 在 kotlinc 過程當中提供 hook 時機,能夠再次期間解析 AST、修改字節碼產物等,Kotlin 的很多語法糖都是 KCP 實現的,例如 data class@Parcelizekotlin-android-extension 等, 現在火爆的 Compose 其編譯期工做也是藉助 KCP 完成的。後端

理論上 KCP 的能力是 KAPT 的超集,能夠替代 KAPT 以提高編譯速度。可是 KCP 的開發成本過高,涉及 Gradle Plugin、Kotlin Plugin 等的使用,API 涉及一些編譯器知識的瞭解,通常開發者很難掌握。api

一個標準 KCP 的開發涉及如下諸多內容:markdown

  • Plugin:Gradle 插件用來讀取 Gradle 配置傳遞給 KCP(Kotlin Plugin)
  • Subplugin:爲 KCP 提供自定義 KP 的 maven 庫地址等配置信息
  • CommandLineProcessor:將參數轉換爲 KP 可識別參數
  • ComponentRegistrar:註冊 Extension 到 KCP 不一樣流程中
  • Extension:實現自定義的 KP 功能

KSP 簡化了上述流程,開發者無需瞭解編譯器工做原理,處理註解等成本像 KAPT 同樣低。app

KSP 與 KAPT

KSP 顧名思義,在 Symbols 級別對 Kotlin 的 AST 進行處理,訪問類、類成員、函數、相關參數等類型的元素。能夠類比 PSI 中的 Kotlin ASTjvm

一個 Kotlin 源文件經 KSP 解析後的結果以下:

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<KSVariableParameter>
      // 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: KSVariableParameter
    KSEnumEntryDeclaration
      // same as KSClassDeclaration
複製代碼

這是 KSP 中的 Kotlin AST 抽象。 相似的, APT/KAPT 中有對 Java 的 AST 抽象,其中能找到一些對應關係,好比 Java 使用 Element 描述包、類、方法或者變量等, KSP 中使用 Declaration

Java/APT Kotlin/KSP Description
PackageElement KSFile 表示一個包程序元素。提供對有關包及其成員的信息的訪問
ExecuteableElement KSFunctionDeclaration 表示某個類或接口的方法、構造方法或初始化程序(靜態或實例),包括註釋類型元素
TypeElement KSClassDeclaration 表示一個類或接口程序元素。提供對有關類型及其成員的信息的訪問。注意,枚舉類型是一種類,而註解類型是一種接口
VariableElement KSVariableParameter / KSPropertyDeclaration 表示一個字段、enum 常量、方法或構造方法參數、局部變量或異常參數

Declaration 之下還有 Type 信息 ,好比函數的參數、返回值類型等,在 APT 中使用 TypeMirror 承載類型信息 ,KSP 中詳細的能力由 KSType 實現。

KSP 的開發流程和 KAPT 相似:

  1. 解析源碼AST
  2. 生成代碼
  3. 生成的代碼與源碼一塊兒參與 Kotlin 編譯

須要注意 KSP 不能用來修改原代碼,只能用來生成新代碼

KSP 入口:SymbolProcessorProvider

KSP 經過 SymbolProcessor 來具體執行。SymbolProcessor 須要經過一個 SymbolProcessorProvider 來建立。所以 SymbolProcessorProvider 就是 KSP 執行的入口

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) {
        //使用 FindFunctionsVisitor 遍歷訪問 AST
        resolver.getAllFiles().map { it.accept(visitor, Unit) }
    }

    inner class FindFunctionsVisitor : KSVisitorVoid() {
        override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
            //訪問 Class 節點
            classDeclaration.getDeclaredFunctions().map { it.accept(this, Unit) }
        }

        override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) {
            // 訪問 function 節點
            functions.add(function)
        }

        override fun visitFile(file: KSFile, data: Unit) {
            //訪問 file
            file.declarations.map { it.accept(this, Unit) }
        }
    }
    ...
}
複製代碼

KSP API 示例

舉幾個例子看一下 KSP 的 API 是如何工做的

訪問類中的全部成員方法

fun KSClassDeclaration.getDeclaredFunctions(): List<KSFunctionDeclaration> {
    return this.declarations.filterIsInstance<KSFunctionDeclaration>()
}
複製代碼

判斷一個類或者方法是不是局部類或局部方法

fun KSDeclaration.isLocal(): Boolean {
    return this.parentDeclaration != null && this.parentDeclaration !is KSClassDeclaration
}
複製代碼

判斷一個類成員是否對其餘Declaration可見

fun KSDeclaration.isVisibleFrom(other: KSDeclaration): Boolean {
    return when {
        // locals are limited to lexical scope
        this.isLocal() -> this.parentDeclaration == other
        // file visibility or member
        this.isPrivate() -> {
            this.parentDeclaration == other.parentDeclaration
                    || this.parentDeclaration == other
                    || (
                        this.parentDeclaration == null
                            && other.parentDeclaration == null
                            && this.containingFile == other.containingFile
                    )
        }
        this.isPublic() -> true
        this.isInternal() && other.containingFile != null && this.containingFile != null -> true
        else -> false
    }
}
複製代碼

獲取註解信息

// Find out suppressed names in a file annotation:
// @file:kotlin.Suppress("Example1", "Example2")
fun KSFile.suppressedNames(): List<String> {
    val ignoredNames = mutableListOf<String>()
    annotations.forEach {
        if (it.shortName.asString() == "Suppress" && it.annotationType.resolve()?.declaration?.qualifiedName?.asString() == "kotlin.Suppress") {
            it.arguments.forEach {
                (it.value as List<String>).forEach { ignoredNames.add(it) }
            }
        }
    }
    return ignoredNames
}
複製代碼

代碼生成的示例

最後看一個相對完整的例子,用來替代APT的代碼生成

@IntSummable
data class Foo(
  val bar: Int = 234,
  val baz: Int = 123
)
複製代碼

咱們但願經過KSP處理@IntSummable,生成如下代碼

public fun Foo.sumInts(): Int {
  val sum = bar + baz
  return sum
}
複製代碼

Dependencies

開發 KSP 須要添加依賴:

plugins {
    kotlin("jvm") version "1.4.32"
}

repositories {
    mavenCentral()
    google()
}

dependencies {
    implementation(kotlin("stdlib"))
    implementation("com.google.devtools.ksp:symbol-processing-api:1.5.10-1.0.0-beta01")
}
複製代碼

IntSummableProcessorProvider

咱們須要一個入口的 Provider 來構建 Processor

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

class IntSummableProcessorProvider : SymbolProcessorProvider {

    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return IntSummableProcessor(
            options = environment.options,
            codeGenerator = environment.codeGenerator,
            logger = environment.logger
        )
    }
}
複製代碼

經過 SymbolProcessorEnvironment 能夠爲 Processor 注入了 optionsCodeGeneratorlogger 等所需依賴

IntSummableProcessor

class IntSummableProcessor() : SymbolProcessor {
    
    private lateinit var intType: KSType

    override fun process(resolver: Resolver): List<KSAnnotated> {
        intType = resolver.builtIns.intType
        val symbols = resolver.getSymbolsWithAnnotation(IntSummable::class.qualifiedName!!).filterNot{ it.validate() }

        symbols.filter { it is KSClassDeclaration && it.validate() }
            .forEach { it.accept(IntSummableVisitor(), Unit) }

        return symbols.toList()
    }
}    
複製代碼
  • builtIns.intType 獲取到 kotlin.IntKSType, 在後面須要使用。
  • getSymbolsWithAnnotation 獲取註解爲 IntSummable 的 symbols 列表
  • 當 symbol 是 Class 時,使用 Visitor 對其進行處理

IntSummableVisitor

Visitor 的接口通常以下,DR 表明 Visitor 的輸入和輸出,

interface KSVisitor<D, R> {
    fun visitNode(node: KSNode, data: D): R

    fun visitAnnotated(annotated: KSAnnotated, data: D): R
    
    // etc.
}
複製代碼

咱們的需求沒有輸入輸出,因此實現KSVisitorVoid便可,本質上是一個 KSVisitor<Unit, Unit>

inner class Visitor : KSVisitorVoid() {
    
    override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
        val qualifiedName = classDeclaration.qualifiedName?.asString()

        //1. 合法性檢查
        if (!classDeclaration.isDataClass()) {
            logger.error(
                "@IntSummable cannot target non-data class $qualifiedName",
                classDeclaration
            )
            return
        }

        if (qualifiedName == null) {
            logger.error(
                "@IntSummable must target classes with qualified names",
                classDeclaration
            )
            return
        }
        
        //2. 解析Class信息
        //...
        
        //3. 代碼生成
        //...
        
    }
    
    private fun KSClassDeclaration.isDataClass() = modifiers.contains(Modifier.DATA)
}
複製代碼

如上,咱們判斷這個Class是否是data class、其類名是否合法

解析Class信息

接下來須要獲取 Class 中的相關信息,用於咱們的代碼生成:

inner class IntSummableVisitor : KSVisitorVoid() {

    private lateinit var className: String
    private lateinit var packageName: String
    private val summables: MutableList<String> = mutableListOf()

    override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
        //1. 合法性檢查
        //...
        
        //2. 解析Class信息
        val qualifiedName = classDeclaration.qualifiedName?.asString()
        className = qualifiedName
        packageName = classDeclaration.packageName.asString()

        classDeclaration.getAllProperties()
            .forEach {
                it.accept(this, Unit)
            }

        if (summables.isEmpty()) {
            return
        }
        
        //3. 代碼生成
        //...
    }
    
    override fun visitPropertyDeclaration(property: KSPropertyDeclaration, data: Unit) {
        if (property.type.resolve().isAssignableFrom(intType)) {
            val name = property.simpleName.asString()
            summables.add(name)
        }
    }
}
複製代碼
  • 經過 KSClassDeclaration 獲取了classNamepackageName,以及 Properties 並將其存入 summables
  • visitPropertyDeclaration 中確保 Property 必須是 Int 類型,這裏用到了前面提到的 intType

代碼生成

收集完 Class 信息後,着手代碼生成。 咱們引入 KotlinPoet 幫助咱們生成 Kotlin 代碼

dependencies {
    implementation("com.squareup:kotlinpoet:1.8.0")
}
複製代碼
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {

    //1. 合法性檢查
    //...
      

    //2. 解析Class信息
    //...
        
        
    //3. 代碼生成
    if (summables.isEmpty()) {
        return
    }

    val fileSpec = FileSpec.builder(
        packageName = packageName,
        fileName = classDeclaration.simpleName.asString()
    ).apply {
        addFunction(
            FunSpec.builder("sumInts")
                .receiver(ClassName.bestGuess(className))
                .returns(Int::class)
                .addStatement("val sum = ${summables.joinToString(" + ")}")
                .addStatement("return sum")
                .build()
        )
    }.build()

    codeGenerator.createNewFile(
        dependencies = Dependencies(aggregating = false),
        packageName = packageName,
        fileName = classDeclaration.simpleName.asString()
    ).use { outputStream ->
        outputStream.writer()
            .use {
                fileSpec.writeTo(it)
            }
    }
}
複製代碼
  • 使用 KotlinPoet 的 FunSpec 生成 function 代碼
  • 前面SymbolProcessorEnvironment 提供的CodeGenerator用來建立文件,並寫入生成的FileSpec代碼

總結

經過 IntSummable 的例子能夠看到 KSP 徹底能夠替代 APT/KAPT 進行註解處理,且性能更出色。

目前,已有很多使用 APT 的三方庫增長了對 KSP 的支持

Library Status Tracking issue for KSP
Room Experimentally supported
Moshi Experimentally supported
Kotshi Experimentally supported
Lyricist Experimentally supported
Auto Factory Not yet supported Link
Dagger Not yet supported Link
Hilt Not yet supported Link
Glide Not yet supported Link
DeeplinkDispatch Not yet supported Link

將 KAPT 替換爲 KSP 也很是簡單,以 Moshi 爲例

固然,也能夠在項目中同時使用 KAPT 和 KSP ,他們互不影響。KSP 取代 KAPT 的趨勢愈來愈明顯,果你的項目也處理註解的需求,不妨試試 KSP ?

github.com/google/ksp

相關文章
相關標籤/搜索