别了 KAPT , 使用 KSP 快速实现 ButterKnife

前言

注解处理器是Android开发中一种常用的技术,很多常用的框架比如ButterKnifeARouterGlide中都使用到了注解处理器相关技术

但是如果项目比较大的话,会很容易发现KAPT是拖慢编译速度的常见原因,这也是谷歌推出KSP取代KAPT的原因

目前KSP已经发布了正式版,越来越多的框架也已经支持了KSP,因此现在应该是时候把你的迁移到KSP了~

本文主要介绍了KSP的一些优势与原理,以及使用KSP快速实现一个简易的ButterKnife框架,以实现KSP的快速上手

为什么使用KSP

KAPT为什么慢?

从上面这张图其实就可以看出原因了,KAPT处理注解的原理是将代码首先生成Java Stubs,再将Java Stubs交给APT处理的,这样天然多了一步,自然就耗时了

同时在项目中可以发现,往往生成Java Stubs的时间比APT真正处理注解的时间要长,因此使用KSP有时可以得到100%以上的速度提升

同时由于KAPT不能直接解析Kotlin的特有的一些符号,比如data class,当我们要处理这些符号的时候就比较麻烦,而KSP则可以直接识别Kotlin符号

KSP是什么

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 2 times faster.

官网对KSP的描述如上,主要说了两点:

  1. KSP是对KCP(Kotlin编译器插件)的轻量化封装,可以在降低我们学习曲线的同时,可以使用到Kotlin编译器的一些能力
  2. 相比于KAPTKSP处理注解可以得到2倍的性能提升

上面得到了KCP(Kotlin编译器插件),KCPkotlinc过程中提供 hook 时机,可以在此期间解析 AST、修改字节码产物等,Kotlin 的不少语法糖都是 KCP 实现的,例如 data class@Parcelizekotlin-android-extension 等, 如今火爆的 Compose 其编译期工作也是借助 KCP 完成的。

KCP虽然强大,但开发成本也很高,学习曲线比较陡峭,因此当我们只需要处理注解等问题时,使用KCP是多余的,于是Google推出了KSP,它基于KCP,但屏蔽了KCP的细节,让我们专注于注解处理的业务

KSP实战

ButterKnife是上古时期比较常用的一个框架,现在有KAEViewBinding了,当然也就用不上了

ButterKnife的主要原理是为注解解析的字段自动生成findViewById的代码,其中主要也是用到了注解处理技术,接下来我们就一起实现一个简易的ButterKnife框架

1. 声明注解

annotation class BindView(val value: Int)

首先要做的就是声明BindView注解

2. 添加ProcessorProvider

class ButterKnifeProcessorProvider : SymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return ButterKnifeProcessor(environment.codeGenerator, environment.logger)
    }
}

ProcessorProvider用于提供注解处理器,其中主要提供了SymbolProcessorEnvironment,主要提供了以下功能

  1. environment.options可以获取build.gradle声明的ksp option
  2. environment.logger提供了logger供我们打印日志
  3. 最常用的是environment.codeGenerator,用于生成与管理文件,不使用此 API 创建的文件将不会参与增量处理或后续编译。

3. 获取注解处理的符号

class ButterKnifeProcessor(
    private val codeGenerator: CodeGenerator,
    private val logger: KSPLogger
) : SymbolProcessor {
    override fun process(resolver: Resolver): List<KSAnnotated> {
        val symbols = resolver.getSymbolsWithAnnotation(BindView::class.qualifiedName!!)
        val ret = symbols.filter { !it.validate() }.toList()
        val butterKnifeList = symbols
            .filter { it is KSPropertyDeclaration && it.validate() }
            .map { it as KSPropertyDeclaration }.toList()
        ButterKnifeGenerator().generate(codeGenerator, logger, butterKnifeList)
        return ret
    }
}

代码其实很简单,找出被BindView注解的符号,并过滤出KSPropertyDeclaration,也就是声明的属性

4. 使用kotlin-poet生成代码

class ButterKnifeGenerator {
    @OptIn(KotlinPoetKspPreview::class)
    fun generate(
        codeGenerator: CodeGenerator, logger: KSPLogger,list: List<KSPropertyDeclaration>
    ) {
    	// 将获取的符号按包名与类名分组
        val map = list.groupBy {
            val parent = it.parent as KSClassDeclaration
            val key = "${parent.toClassName().simpleName},${parent.packageName.asString()}"
            key
        }

        map.forEach {
            val classItem = it.value[0].parent as KSClassDeclaration
            // 添加文件
            val fileSpecBuilder = FileSpec.builder(
                classItem.packageName.asString(),
                "${classItem.toClassName().simpleName}ViewBind"
            )

            // 添加方法
            val functionBuilder = FunSpec.builder("bindView")
                .receiver(classItem.toClassName())

            it.value.forEach { item ->
            	// 获取属性名与注解的值
                val symbolName = item.simpleName.asString()
                val annotationValue =
                    (item.annotations.firstOrNull()?.arguments?.firstOrNull()?.value as? Int) ?: 0
                functionBuilder.addStatement("$symbolName = findViewById(${annotationValue})")
            }

            // 写文件
            fileSpecBuilder.addFunction(functionBuilder.build())
                .build()
                .writeTo(codeGenerator, false)
        }
    }
}

代码也不长,主要分为以下几步:

  1. 因为我们获取的是所有被BindView注解的忏悔,因此需要将获取的符号根据包名与类名分组
  2. 遍历map,生成文件,并在其中生成相应ActivitybindView扩展方法
  3. bindView方法中,利用相关API获取属性名与注解的值
  4. 利用kotlin-poetcodeGenerator生成代码

5. 生成的代码

package com.zj.ksp_butterknife

import kotlin.Unit

public fun MainActivity.bindView(): Unit {
  fabView = findViewById(2131230915)
  toolbar = findViewById(2131231195)
}

build/generated/ksp/debug/kotlin目录下可以看到生成的代码,如上所示,其实就是给MainActivity添加了个扩展方法,在其中会自动为被注解的属性赋值

6. 在项目中使用

plugins {
    id("com.google.devtools.ksp")
}

android {
    kotlin {
        sourceSets {
        	// 让IDE识别KSP生成的代码
            main.kotlin.srcDirs += 'build/generated/ksp'
        }
    }
}

dependencies {
    implementation project(':butterknife-annotation')
    ksp project(':butterknife-ksp-compiler')
}

kapt使用的步骤其实差不多,主要区别在于默认情况下IDE并不认识KSP生成的代码,为了在IDE中支持引用相关的类,需要扩展main.kotlin.srcDirs

总结

本文主要介绍了KSP的一些特性以及如何利用KSP快速实现一个简易的ButterKnifeKSP相比KAPT主要有以下优势

  1. KSP性能更好,有时可以达到2倍的速度提升;
  2. KSP开发起来更加方便,不需要自己处理增量编译逻辑;
  3. KSP支持多平台,而KAPT只支持JVM平台
  4. KSP拥有更符合Kotin习惯的API,同时可以识别Kotin特有的符号

总得来说,KSP目前已经发布正式版了,越来越多的框架也已经支持了KSP,因此现在应该是时候把你的应用迁移到KSP了~

示例代码

本文所有代码可见:github.com/shenzhen201…

参考资料

Kotlin 编译器插件:我们究竟在期待什么?
Kotlin Symbol Processors

猜你喜欢

转载自juejin.im/post/7116305314529411085
今日推荐