preface

Everybody back up. Today I’m going to start a performance of the art of pretending to be a bitch. This time we tried to use KSP (Kotlin Symbol Processing), which Google announced a while back, specifically for the Kotlin project to speed up annotation generation.

Before KSP came out, we used Java’s AbstractProcessor for this annotation interpreter. I’ve written about AbstractProcessor in previous articles. This time we’re going to try something new on Google, because if I say hello fast enough, no one will ever say hello to me.

Kotlin Symbol Processing (KSP) is an API that you can use to develop lightweight compiler plugins. KSP provides a simplified compiler plugin API that leverages the power of Kotlin while keeping the learning curve at a minimum. Compared to KAPT, annotation processors that use KSP can run up to 2x faster.

The official introduction of KSP is that it is a lightweight replacement of KAPT, the advantage is faster, fewer parameters and a little more simple. But the world martial arts only fast not broken ah, compiler speed to improve this kind of thing, after all, it is very difficult.

At the same time, KSP is cooler than KAPT access, and it also supports incremental compilation. If you are interested, please read Google’s description of KSP carefully. Here is the portal.

Tip KSP can actually do the ast in Kapt to modify the operation of class oh, can let you awesome big big man, directly modify the current Kotlin file.

Let’s start the project presentation

This time we started from scratch to implement a KSP compiler, as for the code is still put in our routing component, after all, if you write from scratch with annotations also a little confused, or in the historical Demo development, below is the address oh.

Router address The Router address is as follows

Since the official documentation recommended that we use gradle.kts, we upgraded the project as a whole. In another post to follow in this section, there is no escape from the law of Truth, which is also a little friendlier to development than Groovy, since KTS is still a strongly typed language.

The first step

The first step is relatively simple, mainly to the root directory of the project Gradle configuration changes, so that we can smoothly obtain the KSP reference. Although relatively simple, it was also crucial. It took me about a weekend to complete the transformation of KTS and successfully introduce KSP, although I was taking care of my baby for part of the time.

Add the following code to settings.gralde. KTS in the root directory of your project, especially settings.gradle.kts.

pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        maven("https://dl.bintray.com/kotlin/kotlin-eap")}}Copy the code

This will help us get the dependencies of the KSP plugin. Meanwhile, as KSP relies on kotlin Plugin 1.4.30, we need to upgrade kt plugin version as well.

Add the following code to build.gradle. KTS.

buildscript {
    repositories {
        jcenter()
        google()
    }
    dependencies {
        classpath(kotlin("gradle-plugin", version = "1.4.30"))
    }
}

plugins { 
    // Kotlin 1.4.30 plugin version of life but not directly introduced
    kotlin("jvm") version "1.4.30" apply false
}
Copy the code

The second step

Now that we have completed the first step, we can create a module for kspCompiler. Then we can begin our KSP writing journey.

Take a quick look at build.gradle in our directory. Because we need to obtain the dependency of KSP, and KSP, like KAPT, is loaded through SPI mechanism, we need to introduce a ANNOTATION library of KSP ourselves.

import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion

plugins {
    // This is a Java plugin
    kotlin("jvm")
    // Since we are generating a meta-INF using KSP, we also need the KSP plugin
    id("com.google.devtools.ksp") version "1.4.30-1.0.0 - alpha04"
}


dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib:${getKotlinPluginVersion()}")
    implementation(project(":RouterAnnotation"))
    implementation("Com. Squareup: kotlinpoet: 1.7.2.")
  
    implementation("Com. Google. Auto. Services: auto - service - annotations: 1.0 rc7." ")
    // You must add this and you must compileOnly otherwise there will be a problem
    compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable:${getKotlinPluginVersion()}")
    compileOnly("Dev. Zacsweers. Autoservice: auto - service - KSP: 0.3.2")
    compileOnly("Com. Google. Devtools. KSP: symbol - processing - API: 1.4.30 1.0.0 - alpha04")
    You can create a meta-INF file without using KSP or APT
    ksp("Dev. Zacsweers. Autoservice: auto - service - KSP: 0.3.2")}Copy the code

You must pay attention to the annotation above, because these points are the pits I stepped on. CompileOnly will cause your KSP not to be executed and the compileOnly will cause an error.

My personal guess here is because the implementation will only take effect on the current Module and cannot be passed to other places, so that the corresponding JAR package cannot be referenced in the compilation process. My personal guess is that I did not confirm this part of the guess.

The KSP is responsible for generating the meta-INF folder. Atuoservice is used in many places in the compilation process. Jar packages in Java don’t just have.class and meta-info, this folder comes with some additional information. For example, version information in Koltin is located under this folder. As I mentioned in the previous article, autoService’s service discovery mechanism is based on this file.

So, when you write KSP, you can also generate a meta-info, just like writing plugin. The file name can refer to the picture above.

The third step

At this point we can formally begin to write KSP code. First we need to implement a SymbolProcessor.

interface SymbolProcessor {
    /**
     * Called by Kotlin Symbol Processing to initialize the processor.
     *
     * @param options passed from command line, Gradle, etc.
     * @param kotlinVersion language version of compilation environment.
     * @param codeGenerator creates managed files.
     */
    fun init(options: Map<String, String>, kotlinVersion: KotlinVersion, codeGenerator: CodeGenerator, logger: KSPLogger)

    /**
     * Called by Kotlin Symbol Processing to run the processing task.
     *
     * @param resolver provides [SymbolProcessor] with access to compiler details such as Symbols.
     * @return A list of deferred symbols that the processor can't process.
     */
    fun process(resolver: Resolver): List<KSAnnotated>

    /** * Called by Kotlin Symbol Processing to finalize the processing of a compilation. */
    fun finish(a) {}

    /** * Called by Kotlin Symbol Processing to handle errors after a round of processing. */
    fun onError(a){}}Copy the code

KSP is an upgraded version of Kapt, so the annotation interpreter implementation is basically the same as AbstractProcessor. The init method gets some of the key parameters of the construct, the path to the write file, and so on. Process allows us to get the current abstract syntax tree, then all the corresponding syntax trees annotated with the route, and then further development. So let’s look at the code.

// Check whether the processing is done
private var isload = false
override fun process(resolver: Resolver): List<KSAnnotated> {
    if (isload) {
        return emptyList()
    }
    val symbols = resolver.getSymbolsWithAnnotation(BindRouter::class.java.name)
    routerBindType = resolver.getClassDeclarationByName(
            resolver.getKSNameFromString(BindRouter::class.java.name)
    )?.asType() ?: kotlin.run {
        logger.error("JsonClass type not found on the classpath.")
        return emptyList()
    }
    symbols.asSequence().forEach {
        add(it)
    }
    // logger.error("className:${moduleName}")
    try {
        ktGenerate.generateKt()
        isload = true
    } catch (e: Exception) {
        logger.error(
                "Error preparing :" + " ${e.stackTrace.joinToString("\n")}")}return symbols
}
Copy the code

First of all, we can get the direct syntax tree list of my route from resolver, which is the annotation list we need to deal with later.

Tip Process will be refired if any classes are generated in process. Because of the nature of routing, syntax tree changes do not need to be handled multiple times

Next, we just need to loop through the symbols list to continue the generation of the routing table.

private fun add(type: KSAnnotated) {
        logger.check(type is KSClassDeclaration && type.origin == Origin.KOTLIN, type) {
            "@JsonClass can't be applied to $type: must be a Kotlin class"
        }

        if (type !is KSClassDeclaration) return

        ktGenerate.addStatement(type, routerBindType)
        //class type
        // val id: Array
      
        = routerAnnotation.urls()
      
    }
Copy the code

I then determine the current type from KSAnnotated, determine if there are Java annotations in the current syntax tree, and throw an exception if there are. If all of these conditions are met, we can proceed with the operation of code recording.

class KtGenerate(private var logger: KSPLogger, name: String, private val codeGenerator: CodeGenerator) {

    private val initMethod: FunSpec.Builder = FunSpec.builder("register").apply {
        addAnnotation(AnnotationSpec.builder(JvmStatic::class).build()}private val className = "RouterInit${name.replace("[^0-9a-zA-Z_]+"."")}"
    private val specBuilder = FileSpec.builder("com.kronos.router.register", className)


    var index = 0

    fun addStatement(type: KSClassDeclaration, routerBindType: KSType) {

        valrouterAnnotation = type.findAnnotationWithType(routerBindType) ? :return
        var isRunnable = false

        type.getAllSuperTypes().forEach {
            if(! isRunnable) { isRunnable = it.toClassName().canonicalName == RUNNABLE } }// logger.error("classType:${isRunnable}")
        val urls = routerAnnotation.getMember<ArrayList<String>>("urls")
        if (urls.isEmpty()) {
            return
        }
        val weight: Int = try {
            routerAnnotation.getMember("weight")}catch (e: Exception) {
            0
        }
        val interceptors = try {
            routerAnnotation.getMember<ArrayList<ClassName>>("interceptors")}catch (e: Exception) {
            null
        }

        urls.forEach { url ->
            if (isRunnable) {
                callBackStatement(url, type.toClassName(), weight, interceptors)
            } else {
                normalStatement(url, type.toClassName(), weight, interceptors)
            }
            index++
        }
    }

    private val optionClassName by lazy {
        MemberName("com.kronos.router.model"."RouterOptions")}private val routerMemberName by lazy {
        MemberName("com.kronos.router"."Router")}private fun callBackStatement(url: String, callBack: ClassName, weight: Int, interceptors: ArrayList<ClassName>? {
        val memberName = "option$index"
        initMethod.addStatement("val $memberName =  %M()", optionClassName)
        buildInterceptors(memberName, interceptors)
        initMethod.addStatement("$memberName.weight=$weight")
        initMethod.addStatement("$memberName.callback=%T()", callBack)
        initMethod.addStatement("%M.map(url=%S,options= $memberName)", routerMemberName, url)
    }

    private fun normalStatement(url: String, activity: ClassName, weight: Int, interceptors: ArrayList<ClassName>? {
        val memberName = "option$index"
        initMethod.addStatement("val $memberName =  %M()", optionClassName)
        buildInterceptors(memberName, interceptors)
        initMethod.addStatement("$memberName.weight=$weight")
        initMethod.addStatement("%M.map(url=%S,mClass=%T::class.java,options= $memberName)",
                routerMemberName, url, activity)
    }


    private fun buildInterceptors(memberName: String, interceptors: ArrayList<ClassName>?{ interceptors? .forEach { initMethod.addStatement("$memberName.addInterceptor(%T())", it)
        }
    }

    fun generateKt(a) {
        val helloWorld = TypeSpec.objectBuilder(className)
                .addFunction(initMethod.build())
                .build()
        specBuilder.addType(helloWorld)
        val spec = specBuilder.build()
        val file = codeGenerator.createNewFile(Dependencies.ALL_FILES, spec.packageName, spec.name)
        file.use {
            val content = spec.toString().toByteArray()
            it.write(content)
        }
    }


    companion object {
        const val RUNNABLE = "com.kronos.router.RouterCallback"}}Copy the code

This part is a bit easier. First determine whether the type is Activity or RouterCallbac, and then insert a different registration code depending on the type, but this time I chose KotlinPoet compared to the previous one.

Finally, after completing the loop, you just need to generateKt to complete the generation of the KT class. But please pay attention to the tips I said above.

How to access

Now that we’ve basically finished developing the KSP interpreter, let’s see how it can be used in a project.

import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
plugins {
    id("com.android.application")
    id("com.google.devtools.ksp") version "1.4.30-1.0.0 - alpha04"
}
dependencies {
  ksp(project(":kspCompiler"))}Copy the code

In simple words, just add a similar kapt, here incidentally to you do not know a little knowledge of science. If you use the KSP-related generate technique, you can check the following table of contents to see if any classes are generated.

Take more

I opened KSP and kapt on one of the modules and conducted data tests. The total discovery time of KSP is less than that of KAPT.

The task time of the kspDebugKotlin task is 1262ms, while the total time of the kapt task is 1603ms because it is split into two tasks. Overall, that’s in line with Google’s 25 percent overall speed increase.

conclusion

Hope this article can help you, in fact, if the compilation speed requirements are relatively high projects, or similar to the byte such a large factory. If you are not satisfied with the current performance of Kapt, you can try to develop kspCompiler first. Because this part is class generation, KSP and Kapt can exist in the project at the same time to test the compilation speed by grayscale experiment.