android編譯時注解,老生常談,外面的例子都是bindView,腦殼看疼了,自己學習和編寫下。
而且現在已經進化到kotlin2.0,google也逐漸放棄kapt,進入維護狀態。所以要好好看看本貼。
參考我的工程:
https://github.com/jzlhll/AndroidComponts
ClassNameAnnotations
ClassNameAnnotations-compiler
ClassNameAnnotations-ksp
app
四個模塊。
一、編寫kapt(abstractProcessor)
1. 新建注解的模塊,注意是java/kotlin library:
配置build.gradle:
plugins {id 'java-library'id 'kotlin'
}java {sourceCompatibility = JavaVersion.VERSION_17targetCompatibility = JavaVersion.VERSION_17
}
添加自定義注解的java類:
@Retention(RetentionPolicy.CLASS)
@Target(value = ElementType.TYPE)
public @interface EntroFrgName {
}
這是我的需求,目的就是標記一個類,用來收集所有標注了注解的類,把他們收集成一個List。
2.再創建compiler模塊,也是java/kotlin library:
得到2個模塊。
2.1 gradle:
plugins {id 'java-library'id 'kotlin'
}java {sourceCompatibility = JavaVersion.VERSION_17targetCompatibility = JavaVersion.VERSION_17
}dependencies {implementation project(':ClassNameAnnotations')
}
2.2 配置解析器輔助文件:
這一步可以通過autoservice來配置。查看文章末尾注意事項。
在main下面reosurces/META-INF/services/目錄下,創建文件javax.annotation.processing.Processor
里面寫上com.au.learning.classnamecompiler.MyProcessor ,
就是下面代碼MyProcessor 的類路徑。
2.3 編寫注解解析代碼:
class MyProcessor : AbstractProcessor() {private var processingEnv:ProcessingEnvironment? = nulloverride fun init(processingEnv: ProcessingEnvironment?) {super.init(processingEnv)this.processingEnv = processingEnvprocessingEnv?.messager?.printMessage(Diagnostic.Kind.WARNING, "init...!")}/*** 所支持的注解合集*/override fun getSupportedAnnotationTypes(): MutableSet<String> {return mutableSetOf(EntroFrgName::class.java.canonicalName)}private fun isElementInAnnotations(target:Element, annotations: Set<TypeElement>) : Boolean {for (annotation in annotations) {//匹配注釋if (target == annotation) {return true}}return false}//Element代表程序中的包名、類、方法。即注解所支持的作用類型。fun getMyElements(annotations: Set<TypeElement>, elements: Set<Element?>): Set<TypeElement> {val result: MutableSet<TypeElement> = HashSet()//遍歷包含的 package class methodfor (element in elements) {//匹配 class or interfaceif (element is TypeElement) {for (annotationMirror in element.annotationMirrors) {val found = isElementInAnnotations(annotationMirror.annotationType.asElement(), annotations)if (found) {result.add(element)break}}}}return result}/*** @param annotations 需要處理的注解 即getSupportedAnnotationTypes被系統解析得到的注解* @param roundEnv 注解處理器所需的環境,幫助進行解析注解。*/override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment?): Boolean {val elements = roundEnv?.rootElements?.let {if (annotations != null) {getMyElements(annotations, it)} else {null}}val names = AllEntroFragmentNamesTemplate()if (!elements.isNullOrEmpty()) {for (e in elements) {names.insert(e.qualifiedName.toString())}val code = names.end()processingEnv.filer?.let {try {// 創建一個JavaFileObject來表示要生成的文件val sourceFile: JavaFileObject = it.createSourceFile("com.allan.androidlearning.EntroList", null)sourceFile.openWriter().use { writer ->// 寫入Java(或Kotlin)代碼writer.write(code)writer.flush()}} catch (e: IOException) {processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, "Failed to generate file: " + e.message)}}}return true}//一定要修改這里,避免無法生效override fun getSupportedSourceVersion(): SourceVersion {return SourceVersion.latestSupported()}
}class AllEntroFragmentNamesTemplate : AbsCodeTemplate() {private val insertCode = StringBuilder()/*** com.allan.androidlearning.activities.LiveDataFragment.class*/fun insert(javaClass:String) {insertCode.append("list.add(").append(javaClass).append(".class);").appendLine()}fun end() : String {return codeTemplate.replace("//insert001", insertCode.toString())}override val codeTemplate = """
package com.allan.androidlearning;import androidx.fragment.app.Fragment;import java.util.ArrayList;
import java.util.List;public class EntroList {public List<Class<? extends Fragment>> getEntroList() {List<Class<? extends Fragment>> list = new ArrayList<>();//insert001return list;}
}""".trimIndent()
}
這里有2個可以進一步學習的東西,一是auto庫幫你生成META-INF文件。
二是通過javapoet來生成文件。詳細在文章末尾注意事項。
本質上APT的目的就是將未知的代碼,寫成一個具體的類,被現有代碼去調用,我自然可以直接寫出這個類。所以,我為了方便和減少學習成本,自行整了一個模版代碼(這個模版代碼可以自己寫好一個類,拷貝到string codeTemplate),把生成部分通過string.replace處理即可。然后簡單地通過processingEnv.filer.createSourceFile,write就可以完成,自認為是一個不錯的辦法。
3. 主工程
剩下就簡單了,app/build.gradle修改:
plugins {id 'com.android.application'id 'org.jetbrains.kotlin.android'id 'kotlin-kapt' //添加}...//注解引如implementation project(':ClassNameAnnotations')//kotlinkapt project(':ClassNameAnnotations-compiler')//java工程換成annotationProcessor //annotationProcessor project(':ClassNameAnnotations-compiler')
給代碼添加自己的注解了:
@EntroFrgName
class CanvasFragment : ViewFragment() {@EntroFrgName
class DialogsFragment : ViewFragment() {
編譯:
調試過程,可以選擇gradle->Tasks->other->kaptDebugKotlin來編譯。比直接編譯更快,更單一。
編譯結果在:
再最后,把這個類,拿去類似BuildConfig一樣去調用了。至此已經完成。
二、app模塊是java工程
自然是用不了ksp的。
唯一修改是app/build.gradle:
//java工程換成annotationProcessor annotationProcessor project(':ClassNameAnnotations-compiler')
然后各個gradle中,無需kotlin相關的痕跡。略。
三、KSP
終于談到ksp了。
跟上面kapt一樣,創建2個java/kotlin的模塊。一個注解模塊,一個處理模塊,(那個灰色的compiler代表著settings.gradle已經不加載,不使用,不管它)。
注解模塊的注解可以使用kotlin的注解類,也可以繼續使用java的注解類。
區別只是在provider的解析代碼上有一點點區別:
//EntroFrgName是java的注解類
resolver.getSymbolsWithAnnotation(EntroFrgName::class.java.canonicalName)
//EntroFrgName是kotlin的注解類
resolver.getSymbolsWithAnnotation(EntroFrgName::class.qualifiedName!!)
1. gradle:
根目錄的build.gradle添加:
plugins {id 'com.android.application' version '8.4.2' apply falseid 'com.android.library' version '8.4.2' apply falseid 'org.jetbrains.kotlin.android' version "1.9.24" apply falseid 'com.google.devtools.ksp' version '1.9.24-1.0.20' apply false
}
ksp模塊的build.gradle為:
plugins {id 'java-library'id 'kotlin'
}java {sourceCompatibility = JavaVersion.VERSION_17targetCompatibility = JavaVersion.VERSION_17
}dependencies {implementation project(':ClassNameAnnotations')implementation('com.google.devtools.ksp:symbol-processing-api:1.9.24-1.0.20')
}
注意kotlin.android, devtools.ksp與symbol-processing-api三者的版本對應,查看https://github.com/google/ksp/releases。
2. 配置解析器輔助文件:
src/main/resources/META-INF/services/目錄下:
com.google.devtools.ksp.processing.SymbolProcessorProvider 文件。寫下如下的名字。
com.au.learning.classnamecompiler.AllEntroFrgNamesProvider。就是下面的類名。
3. provider解析代碼:
class AllEntroFrgNamesProvider : SymbolProcessorProvider{override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {return TestKspSymbolProcessor(environment)}
}/*** creator: lt 2022/10/20 lt.dygzs@qq.com* effect : ksp處理程序* warning:*/
class TestKspSymbolProcessor(private val environment: SymbolProcessorEnvironment) : SymbolProcessor {// 使用一個集合來跟蹤已經處理過的符號private val processedSymbols = mutableSetOf<KSDeclaration>()override fun process(resolver: Resolver): List<KSAnnotated> {environment.logger.warn("process start....")val symbols = resolver.getSymbolsWithAnnotation(EntroFrgName::class.java.canonicalName)val ret = mutableListOf<KSAnnotated>()val allEntroFragmentNamesTemplate = AllEntroFragmentNamesTemplate()var hasMy = falsesymbols.toList().forEach { symbol->if (!symbol.validate())ret.add(symbol)else {if (symbol is KSClassDeclaration && symbol.classKind == ClassKind.CLASS) {val qualifiedClassName = symbol.qualifiedName?.asString()allEntroFragmentNamesTemplate.insert(qualifiedClassName!!)hasMy = true
// symbol.accept(TestKspVisitor(environment), Unit)//處理符號} else {ret.add(symbol)}}}if (hasMy) {val code = allEntroFragmentNamesTemplate.end()// 生成文件val file = environment.codeGenerator.createNewFile(dependencies = Dependencies(false),packageName = "com.allan.androidlearning",fileName = "EntroList")// 寫入文件內容OutputStreamWriter(file).use { writer ->writer.write(code)}}//返回無法處理的符號return ret}
}
4. 主工程app引入
類似前面kapt的,主工程app/build.gradle
plugins {id 'com.android.application'id 'org.jetbrains.kotlin.android'id 'com.google.devtools.ksp'
}implementation project(':ClassNameAnnotations')ksp project(':ClassNameAnnotations-ksp')
添加注解,編譯后,最終生成的代碼在:
注意事項
1. 注意點
1.1 打印日志用warn。android studio編譯是默認不打印低級別的。
//Processor
processingEnv?.messager?.printMessage(Diagnostic.Kind.WARNING, "init...!")
//ksp
environment.logger.warn("process start....")
1.2 kapt已經逐漸放棄,kt2.0開始不再努力維護kapt。盡量遷移ksp。更快更有支持。
1.3 很多人使用glide,經常把kapt,annotationProcessor,ksp搞混。
我們可以看到,glide庫:
它也是有2個process的模塊的,一個是給老的kapt或者java(annotationProcessor)處理。一個是給ksp。我們如出一轍。
2. 進一步學習
第一個:
使用autoservice來自動注解MyProcessor ,讓它幫我們生成META-INF里面的文件。這個autoservice就干這么點點事情。compiler這個模塊添加gradle(自己在這里看最新版本,https://github.com/google/auto):
annotationProcessor 'com.google.auto.service:auto-service:1.11.0'implementation 'com.google.auto.service:auto-service-annotations:1.11.0'
然后給我們的Processor類添加上注解:
@AutoService(value = {Processor.class})
這純屬于是,我還沒有編寫完自己的注解, 就已經使用上別的注解來給我的注解模塊生成文件了。[手動狗頭]。
第二個,使用javapoet來實現生成代碼。需要自行了解他的api和class,函數的結構。有點學習成本。
3. 坑了一天
出現一個問題,始終找不到原因。原來是
META-INF下面是目錄services,再放一個文件。之前搞成了META-INF.services這個錯誤的目錄!
而studio中顯示的卻跟包名一樣。導致ksp的時候,搞了好久一直編譯不過,提示[ksp] No providers found in processor classpath。好在有這句話,終于在ksp下解決了,之后反推到kapt也解決了。之前搞kapt,怎么都搞不好,也沒有提示。