別了 KAPT , 使用 KSP 快速實現 ButterKnife

語言: CN / TW / HK

theme: smartblue highlight: a11y-dark


前言

註解處理器是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. 聲明註解

kotlin annotation class BindView(val value: Int)

首先要做的就是聲明BindView註解

2. 添加ProcessorProvider

kotlin 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. 獲取註解處理的符號

kotlin 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生成代碼

```kotlin class ButterKnifeGenerator { @OptIn(KotlinPoetKspPreview::class) fun generate( codeGenerator: CodeGenerator, logger: KSPLogger,list: List ) { // 將獲取的符號按包名與類名分組 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. 生成的代碼

```kotlin 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了~

示例代碼

本文所有代碼可見:https://github.com/shenzhen2017/ksp-butterknife

參考資料

Kotlin 編譯器插件:我們究竟在期待什麼?
Kotlin Symbol Processors

我正在參與掘金技術社區創作者簽約計劃招募活動,點擊鏈接報名投稿