Bid farewell to KAPT and use KSP to speed up Android compilation

Posted by manmadareddy on Tue, 04 Jan 2022 03:17:03 +0100

1, KSP

When developing Android applications, many people make complaints about Kotlin slow, and KAPT is one of the culprits who slow down compilation. We know that many Android libraries use annotations to simplify template code, such as Room, Dagger, Retrofit, etc. by default, kotlin uses kapt to process annotations. Kapt has no special annotation processor and needs to be implemented with the help of APT. therefore, it needs to be transformed into APT resolvable 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 stub s. The compilation speed is more than twice that of KAPT. In addition to greatly improving the construction speed of Kotlin developers, the tool also provides support for Kotlin/Native and Kotlin/JS.

2, KSP and KCP

The Kotlin Compiler Plugin is mentioned here. KCP provides Hook opportunities in the Kotlin process, during which AST can be parsed and bytecode products can be modified. Many syntax sugars of Kotlin are implemented by KCP. For example, data class, @ Parcelize, Kotlin Android extension, etc. today's popular Jetpack Compose is also completed with the help of KCP.

Theoretically, the capability of KCP is a superset of KAPT, which can completely replace KAPT to improve the compilation speed. However, the development cost of KCP is too high, involving the use of Gradle Plugin and Kotlin Plugin. The API involves some knowledge of compiler, which is difficult for ordinary developers to master. A standard KCP architecture is shown below.


The above figure involves several specific concepts:

  • Plugin: the Gradle plug-in is used to read the Gradle configuration and pass it to the KCP (Kotlin Plugin);
  • Subplugin: provide KCP with configuration information such as maven library address of custom Kotlin Plugin;
  • CommandLineProcessor: convert parameters to parameters recognized by Kotlin Plugin;
  • ComponentRegistrar: register Extension to different processes of KCP;
  • Extension: realize the customized Kotlin Plugin function;

KSP simplifies the whole 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

KSP, as its name implies, processes the ast of Kotlin at the Symbols level, and accesses elements of types such as classes, class members, functions, and related parameters. It can be compared with Kotlin AST in PSI. The structure is shown in the figure below.

As you can see, the Kotlin AST obtained from a Kotlin source file 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 corresponding relationships. 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 types and their members. Note that the enumeration type is a kind, and the annotation type is an interface
VariableElementKSVariableParameter / KSPropertyDeclarationA field, enum constant, method or constructor parameter, local variable, or exception parameter

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

4, KSP entry SymbolProcessorProvider

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

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

SymbolProcessorEnvironment is mainly used to obtain the dependencies of KSP runtime and inject them into the 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. The Resolver uses the visitor pattern to traverse the AST. As follows, Resolver uses FindFunctionsVisitor to find the function and Class member method of the top level 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 = ...
    }
}

5, Get started quickly

5.1 creating a processor

First, create an empty gradle project.

Then, specify the version of the Kotlin plug-in in the root project for use in other project modules, such as.

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 version of Kotlin in the project, you can use gradle Unified configuration in the 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 in the module build gradle. Add the following steps to the KTS file.

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. The following points should be noted during use:

  • Use symbolprocessorprovider Create() to create a SymbolProcessor. The dependencies required by the processor can be accessed through the symbolprocessorprovider Pass the parameters provided by create().
  • The main logic should be in symbolprocessor Process () method.
  • Use the resolver Getsymbolswitchannotation () to get what we want to process, provided we give the fully qualified name of the annotation, such as com example. annotation. Builder.
  • A common use case of KSP is to implement a custom accessor, interface com google. devtools. ksp. symbol. Ksvisitor, used to manipulate symbols.
  • Information about SymbolProcessorProvider and SymbolProcessor interface Use example , see the following file in the sample project: Src / main / kotlin / builderprocessor KT and Src / main / kotlin / testprocessor kt.
  • After writing your own processor, go to resources / meta-inf / services / com google. devtools. ksp. processing. The symbolprocessorprovider contains the fully qualified name of the processor provider and registers it in the package.

5.2 using processor

5.2. 1 use Kotlin DSL

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

pluginManagement {
    repositories {
       gradlePluginPortal()
    }
}

In the new module build In gradle, we mainly accomplish the following things:

  • Apply com.com with the specified version google. devtools. KSP plug-in.
  • Add ksp to dependency list

For example:

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

Run/ gradlew command to build, you can find the generated code under build/generated/source/ksp. Here is a build gradle. KTS example of applying KSP plug-in to 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 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")
}

The symbol processor environment provides the processors option, which is specified in the gradle build script.

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

5.3 generating code using IDE

By default, IntelliJ or other ide s are unaware of the generated code, so references to these generated symbols will be marked as non resolvable. 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 plug-in, the above code snippet gives 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 to 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 third-party 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

Topics: Android