1

1. KSP

When developing Android applications, many people complain about the slow compilation speed of Kotlin, and KAPT is one of the culprits that slow down the compilation. We know that many Android libraries use annotations to simplify template code, such as Room, Dagger, Retrofit, etc., and Kotlin uses KAPT to process annotations by default. KAPT does not have a special annotation processor, it needs to be implemented with APT, so it needs to generate APT parseable stub (Java code), which slows down the overall compilation speed of Kotlin.

KSP was born under this background. It is based on Kotlin Compiler Plugin (KCP) and does not need to generate additional stubs. The compilation speed is more than twice that of KAPT. In addition to greatly improving the build speed of Kotlin developers, the tool also provides support for Kotlin/Native and Kotlin/JS.

2. KSP and KCP

Kotlin Compiler Plugin is mentioned here. KCP provides Hook opportunities during the kotlinc process, during which it can parse AST and modify bytecode products. Many of Kotlin's syntactic sugar are implemented by KCP. For example, data class, @Parcelize, kotlin-android-extension, etc. Today's popular Jetpack Compose is also completed with KCP.

In theory, KCP's capabilities are a superset of KAPT, which can completely replace KAPT to improve compilation speed. However, the development cost of KCP is too high, involving the use of Gradle Plugin, Kotlin Plugin, etc. The API involves some understanding of compiler knowledge, which is difficult for general developers to master. A standard KCP architecture is shown below.

在这里插入图片描述
Several specific concepts are involved in the figure above:

  • Plugin : Gradle plug-in is used to read Gradle configuration and pass it to KCP (Kotlin Plugin);
  • Subplugin : Provide KCP with configuration information such as the address of the maven library of the custom Kotlin Plugin;
  • CommandLineProcessor : Convert parameters to Kotlin Plugin recognizable parameters;
  • ComponentRegistrar : Register Extension in different processes of KCP;
  • Extension : realize custom Kotlin Plugin function;

KSP simplifies the entire process of KCP, developers do not need to understand the working principle of the compiler, and the cost of processing annotations has become as low as KAPT.

3. KSP and KAPT

As the name implies, KSP processes Kotlin's AST at the Symbols level, accessing elements of types, class members, functions, and related parameters. It can be compared to Kotlin AST in PSI, the structure is as shown in the figure below.

在这里插入图片描述

As you can see, the Kotlin AST obtained after a Kotlin source file is parsed by KSP is as follows.

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

Similarly, APT/KAPT is an abstraction of Java AST. We can find some correspondences. For example, Java uses Element to describe packages, classes, methods, or variables, and KSP uses Declaration.

Java/APTKotlin/KSPdescribe
PackageElementKSFileA package program element that provides access to information about the package and its members
ExecuteableElementKSFunctionDeclarationA method, constructor, or initializer (static or instance) of a class or interface, including annotation type elements
TypeElementKSClassDeclarationA class or interface program element. Provides access to information about the type and its members. Note that the enumeration type is a class, and the annotation type is an interface
VariableElementKSVariableParameter / KSPropertyDeclarationA field, enum constant, method or constructor parameter, local variable or abnormal parameter

There is also Type information under Declaration, such as function parameters, return value types, etc. TypeMirror is used in APT to carry type information, and the detailed capabilities in KSP are implemented by KSType.

Four, KSP entrance SymbolProcessorProvider

The entrance of KSP is in SymbolProcessorProvider, the code is as follows:

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

SymbolProcessorEnvironment is mainly used to obtain KSP runtime dependencies and inject them into Processor:

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

The process() method needs to provide a Resolver to parse the symbols on the AST, and the Resolver uses the visitor pattern to traverse the AST. As follows, the Resolver uses FindFunctionsVisitor to find the top-level functions and Class member methods in the current KSFile.

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 = ...
    }
}

Five, get started quickly

5.1 Create a processor

First, create an empty gradle project.
在这里插入图片描述
Then, specify the version of the Kotlin plugin in the root project for use in other project modules, for example.

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

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

However, in order to unify the Kotlin version in the project, a unified configuration can be made in the gradle.properties file.

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

Next, add a module to host the processor. And add the following steps in the build.gradle.kts file of the module.

plugins {
    kotlin("jvm")
}

repositories {
    mavenCentral()
}

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

Next, we need to implement com.google.devtools.ksp.processing.SymbolProcessor and com.google.devtools.ksp.processing.SymbolProcessorProvider. The implementation of SymbolProcessorProvider is loaded as a service to instantiate the implemented SymbolProcessor. Pay attention to the following points when using:

  • Use SymbolProcessorProvider.create() to create a SymbolProcessor. The dependencies required by the processor can be passed through the parameters provided by SymbolProcessorProvider.create().
  • The main logic should be executed in the SymbolProcessor.process() method.
  • Use resoler.getsymbolswithannotation() to get the content we want to process, provided that the fully qualified name of the annotation is given, such as com.example.annotation.Builder .
  • A common use case of KSP is to implement a customized accessor, the interface com.google.devtools.ksp.symbol.KSVisitor , which is used to manipulate symbols.
  • usage example of SymbolProcessorProvider and SymbolProcessor interface , please refer to the following files in the sample project: src/main/kotlin/BuilderProcessor.kt and src/main/kotlin/TestProcessor.kt .
  • After writing your own processor, include the fully qualified name of the processor provider resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider

5.2 Use processor

5.2.1 Using Kotlin DSL

Create another module that contains the work that the processor needs to handle. Then, add the following code in the build.gradle.kts file.

pluginManagement {
    repositories {
       gradlePluginPortal()
    }
}

In the build.gradle of the new module, we mainly complete the following things:

  • Apply the com.google.devtools.ksp plugin with the specified version.
  • Add ksp to the list of dependencies

for example:

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

Run the ./gradlew command to build, you can find the generated code under build/generated/source/ksp The following is an example of applying the KSP plugin to the workload in build.gradle.kts.

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 Using Groovy

Build in your project. The Gradle file adds a plug-in block containing the KSP plug-in

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

Then, add the following dependencies in dependencies.

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

SymbolProcessorEnvironment provides processors options, which are specified in the gradle build script.

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

5.3 Use IDE to generate code

By default, IntelliJ or other IDEs are not aware of the generated code, so references to these generated symbols will be marked as unresolvable. In order for IntelliJ to operate on the generated code, the following configuration needs to be added.

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

Of course, it can also be a resource directory.

build/generated/ksp/main/resources/

When using, you also need to configure these generated directories in the KSP processor module.

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

If IntelliJ IDEA and KSP are used in the Gradle plugin, the above code snippet will give the following warning:

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

For this warning, we can add the following code in the module.

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")
    }
}

At present, many tripartite libraries using APT have added support for KSP, as follows.

LibraryStatusTracking issue for KSP
RoomExperimentally supported
MoshiOfficially supported
RxHttpOfficially supported
KotshiOfficially supported
LyricistOfficially supported
Lich SavedStateOfficially supported
gRPC DekoratorOfficially supported
Auto FactoryNot yet supportedLink
DaggerNot yet supportedLink
HiltNot yet supportedLink
GlideNot yet supportedLink
DeeplinkDispatchSupported via airbnb/DeepLinkDispatch#323

xiangzhihong
5.9k 声望15.3k 粉丝

著有《React Native移动开发实战》1,2,3、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台开发与实战》1,2和《Android应用开发实战》