APT是什么?
APT全称Annotation Processing Tool,即注解处理器,是一种用来处理注解的工具。在JVM将java文件编译成class文件前便扫描java文件并处理注解生成文件。由于是编译前处理,故通常用来生成源码文件,无论是Java还是Kotlin文件。
APT能干什么?
在编译流程前,代码能干的事儿它都能干,主要用来生成文件,源码文件或者配置文件都行。
APT的优缺点?
- 优点:很明显,能在进入编译流程前做一些简单的处理,对于有编译前处理文件需求的人来说是个福音
- 缺点:它的优点当然也是它的缺点,即不能处理编译流程之后的事儿
如何使用APT?
声明注解
新建一个Java library,命名为apt-annotation,并新建一个注解类annotation class BindView,这里我们模仿ButterKnife为class命名。注解类主要内容如下
package com.kaithmy.apt_annotation
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FIELD)
annotation class BindView(
val value: Int
)
其中变量value我们用于存储需要绑定到组件变量的组件id,如@BindView(R.id.tvSend),当然了,AGP(Android Gradle Plugin)在5.0 之后不再保证生成的资源id是不可变的,这里只是为了演示,先这么用吧。如需要类似于ButterKnife的功能,推荐使用Kotlin Android Extension,虽然官方最近也准备将其废弃转为推广ViewBinding。
声明注解处理器
新建一个Java library,命名为apt-processor,新建一个注解处理类BindViewProcessor,我们使其
继承抽象类AbstractProcessor,并重写其中的两个方法init和process。顾名思义,init即为我们的初始化方法,process则为我们处理注解的主要方法,我们处理注解的逻辑主要都在process方法里。同时我们还要重写getSupportedAnnotationTypes方法,他的返回值代表了我们能处理那些注解类型,当然我们也可以重写getSupportedSourceVersion方法来声明支持的JDK版本,这里我们声明支持JDK1.8。
// 支持的注解类型,可以使用@SupportedAnnotationTypes("com.kaithmy.apt_annotation.BindView")代替
override fun getSupportedAnnotationTypes(): MutableSet<String> {
return mutableSetOf(BindView::class.java.canonicalName)
}
// 支持的JDK版本,可以使用@SupportedSourceVersion(SourceVersion.RELEASE_8)代替
override fun getSupportedSourceVersion(): SourceVersion {
return SourceVersion.RELEASE_8
}
如果嫌弃每次都要重写这两个方法麻烦,我们可以使用注解代替,将注解添加到我们的BindViewProcessor上,如
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.kaithmy.apt_annotation.BindView")
class BindViewProcessor : AbstractProcessor() {
.......
}
认识init方法及其主要四个工具类
init方法中会传给我们一个ProcessingEnvironment类型的参数processingEnv,顾名思义,它是注解处理环境。
调用其getFiler方法能得到一个Filer类型的filerUtils,即文件管理工具类,操作文件相关的类;
调用其
getTypeUtils方法能得到一个Types类型的typeUtils,即类型处理工具类,主要用于处理类型相关的类;
调用其
getElementUtils方法得到一个Elements类型的elementUtils,即Element处理工具类,主要用于处理由类结构化的Element
调用其
getMessager方法得到一个Messager类型的messagerUtils,即日志工具类,主要用于输出处理过程中的日志
声明api入口
我们先创建一个接口IBindHelper,并提供一个inject方法,
代码如下:
package com.kaithmy.apt_api
interface IBindHelper {
fun inject(target: Any?)
}
然后创建一个object类型的类KBind,并提供inject方法(可以是其他名字,莫要与接口中的inject搞混),代码如下:
fun inject(target: Any?) {
target ?: return
val qualifiedName = target::class.qualifiedName
val helperClassName = "$qualifiedName\$AutoBind"
val helperInstance =
Class.forName(helperClassName).getConstructor().newInstance() as? IBindHelper ?: return
helperInstance.inject(target)
}
大意是通过传递进来的对象的类名qualifiedName找到对应生成的类helperClassName,并通过Class.forName()创建对应示例,再调用其中的inject方法,inject方法中进行findViewById操作并赋值,进而实现注入。
认识process方法,找到注解并保存注解信息
具体流程详见以下代码:
/**
* Note 1:
* process()执行两次是因为首次输入class未输出文件,第二次输入为空输出为空
*
* Note 2:
* 返回值true代表注解已声明处理,并不要求后续Processor处理他们;
* false则表示注解未声明,并可能要求后续Processor处理他们
*/
override fun process(
annotations: MutableSet<out TypeElement>?,
roundEnv: RoundEnvironment?
): Boolean {
println("process start ========")
if (annotations.isNullOrEmpty() || roundEnv == null) return false
// 被BindView注解的所有Element
val bindViewElements = roundEnv.getElementsAnnotatedWith(BindView::class.java)
// 通过不同的宿主进行分类,如AActvity,BActivity,AFragment等
categories(bindViewElements)
bindMap.forEach {
// 生成相应的代码
generateCode(it)
}
println("process end ========")
return true
}
/**
* 收集被注解的信息
*/
private fun categories(bindViewElements: MutableSet<out Element>) {
// 过滤出被BindView注解的变量element
bindViewElements.forEach {
// 获取对应的上一级element
val enclosingElement = it.enclosingElement
// 将views放入bindMap
var views = bindMap[enclosingElement]
if (views == null) {
views = hashSetOf()
bindMap[enclosingElement] = views
}
// 将变量名与id对应起来放入views
val annotation = it.getAnnotation(BindView::class.java)
val id = annotation.value
views.add(
ViewInfo(it.simpleName.toString(), id, it.asType())
)
}
}
生成目标文件
通过以上步骤,我们已经将对应的注解信息保存了下来,接下来我们需要做的便是生成代码了,生成代码目前由三种方式,其中一种是直接通过StringBuilder字符串拼接生成目标代码,另一个是通过辅助工具来生成目标代码,由于使用的语言不同,故又细分为二,一是JavaPoet,二是KotlinPoet
JavaPoet生成
val SUFFIX = "\$AutoBind"
val elementName = entry.key.simpleName.toString()
val packageName = elementUtils.getPackageOf(entry.key).qualifiedName.toString()
val methodSpecBuilder = MethodSpec.methodBuilder("inject")
.addAnnotation(Override::class.java)
.addModifiers(Modifier.PUBLIC)
.addParameter(Any::class.java, "target")
.addCode("$elementName ins = ($elementName)target; \n")
entry.value.forEach {
methodSpecBuilder
.addCode(
"ins.set${
it.viewName.replaceFirstChar { first ->
first.toUpperCase()
}
}(ins.findViewById(${it.resId})); \n"
)
}
val typeSpec = TypeSpec.classBuilder("$elementName$SUFFIX")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(IBindHelper::class.java)
.addMethod(methodSpecBuilder.build())
.build()
JavaFile.builder(packageName, typeSpec).build().writeTo(filerUtils)
KotlinPoet生成
val funBuilder = FunSpec.builder("inject")
.addModifiers(KModifier.OVERRIDE)
.addParameter("target", Any::class.asTypeName().copy(nullable = true))
.addCode("val ins = target as? $elementName?:return \n")
entry.value.forEach {
funBuilder.addCode(
"ins.${it.viewName} = ins.findViewById<${it.type}>(${it.resId}) \n"
)
}
val fileSpec = FileSpec.builder(packageName, "$elementName$SUFFIX")
.addType(
TypeSpec.classBuilder("$elementName$SUFFIX")
.addSuperinterface(IBindHelper::class)
.addFunction(funBuilder.build())
.build()
).build()
fileSpec.writeTo(filerUtils)
通过SPI机制让processor生效
SPI,即Service Provicer Interface,JDK内置的服务发现机制。在模块apt-processor的src/main中,我们新建一个文件夹META-INF,在其中新建一个子文件夹services,并在services新建一个文件javax.annotation.processing.Processor,并在文件里写入我们的BindViewProcessor的全类名com.kaithmy.apt_processor.BindViewProcessor,至此,我们的BindViewProcessor已生效。
通过AutoServcie避免繁琐的SPI过程
总觉得以上的操作有点繁琐,那么有什么方法避免这繁琐的步骤呢?我们可以通过引入AutoServcie来避免。首先在apt-processor中的dependencies引入AutoServices依赖,如:
api "com.google.auto.service:auto-service:1.0-rc7"
kapt "com.google.auto.service:auto-service:1.0-rc7"
然后在我们的BindViewProcessor上加上类注解@AutoService(Processor::class)即可
如何对进行调试?
当我们需要对processor进行调试该如何操作呢?
配置gradle.properties
将以下内容加入project下的gradle.properties中,
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
-Dorg.gradle.debug=true
配置Remote Debug
在Run/Debug Configurations中 新建一个Remote并命名为任意你想要的名字,这里命名为ProcessorDebug,点击ok保存好后到IDE中选中刚刚新建的ProcessorDebug并点击run或者debug启动。
断点并build触发
先通过工具栏clean project清除已有的build文件夹防止之前生成的文件造成影响,然后在你想要断点的位置打上断点,正常执行build/assemble即可,或者在terminal中执行命令./gradlew app:build便可正常调试了。
如何接入自定义processor并使用?
在你的app moudle中引入apt-annotation和apt-processor两个moudle,apt-processor引入时需注意Java使用annotationProcessor,Kotlin使用kapt。
然后在你需要使用注解的地方加上注解即可,示例如下:
class MainActivity : AppCompatActivity() {
@BindView(R.id.tvOpen)
var tvOpen: TextView? = null
@BindView(R.id.tvClose)
var tvClose: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
KBind.bind(this)
tvOpen?.setOnClickListener {
Toast.makeText(this, "Open clicked", Toast.LENGTH_SHORT).show()
}
}
}
Tips
如何监控各个Gradle Task 耗时并统计总编译耗时?
Gradle其实有提供TaskExecutionListener和BuildListener用于Task的执行回调,通过这两个回调接口我们可以很方便地统计各个Task耗时及总耗时。Task细节就不展开讲了,后面讲trasform再细说,直接在project下的build.gradle中加入以下代码即可
import java.util.concurrent.TimeUnit
class TimingsListener implements TaskExecutionListener, BuildListener {
private long startTime
private timings = []
@Override
void beforeExecute(Task task) {
startTime = System.nanoTime()
}
@Override
void afterExecute(Task task, TaskState taskState) {
def ms = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS)
timings.add([ms, task.path])
task.project.logger.log(LogLevel.WARN, "${task.path} took ${ms}ms")
}
@Override
void buildStarted(Gradle gradle) {
}
@Override
void settingsEvaluated(Settings settings) {
}
@Override
void projectsLoaded(Gradle gradle) {
}
@Override
void projectsEvaluated(Gradle gradle) {
}
@Override
void buildFinished(BuildResult buildResult) {
println("Task timings")
for (timing in timings) {
printf "%sms %s\n", timing
}
}
}
gradle.addListener(new TimingsListener())