PS:本文讲解的
APT
全称为Annotation Processing Tool
,而非是Android Performance Tuner
,这两种工具简称皆为APT
,前者是“注释处理工具”,后者是“Android性能调试器”。
本文分别使用
Java
、kotlin
语言进行开发,代码已开源 👉 APT-Demo
目录
- APT
- 仿ButterKnife组件绑定
- 创建类
- 注册类
- SPI
- AutoService
- JavaPoet
- 最终效果
APT
APT
全称为Annotation Processing Tool
,是Javac(Java编译器)
的一个工具。通过APT
,在Javac
编译代码时可以通过注解拿到被注解的对象信息,根据需求自动生成模板代码。
PS:
APT
获取注解和生成代码都是在代码编译时期完成的。
APT
是 Javac
的一个工具,使用的时候在 build.gradle
引入 java-library
插件,使用 java-library
插件的 Module
不应再使用与android
相关的插件,避免报The 'java' plugin has been applied, but it is not compatible with the Android plugins.
的兼容错误,这也就是为啥建个package
就能解决的事情,非得建一个Module
的原因。
plugins {
id 'java-library'
}
仿ButterKnife组件绑定
创建类
- BindView
注解类,用于绑定的的xml
资源id
的存、取。
@Retention(RetentionPolicy.CLASS)
@Target(AnnotationTarget.FIELD)
annotation class BindView(val value: Int)
@Retention(RetentionPolicy.CLASS):表示这个注解保留到编译期
@Target(ElementType.FIELD):表示注解范围为类成员(构造方法、方法、成员变量)
- AutoBind
获取需要生成类的类名,拼接成使用APT
生成的类文件名称,通过反射机制传值给APT
生成类的inject
方法。
object AutoBind {
fun inject(target: Any) {
val className = target.javaClass.canonicalName
val helperName = "$className$\$Processor"
try {
val helper = Class.forName(helperName).getConstructor().newInstance() as IBindHelper
helper.inject(target)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
- BindViewProcessor
用于生成模板类及代码
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.binyouwei.lib_apt.annotation.BindView")
class BindViewProcessor : AbstractProcessor() {
// 存储绑定的组件信息
private val mBindActivity: MutableMap<TypeElement, MutableSet<ViewInfo>> = hashMapOf()
// 处理工具类
private var mElement: Elements? = null
// 文件管理工作类
private var mFiler: Filer? = null
private var mTypeUtils : Types ? = null
override fun init(processingEnv: ProcessingEnvironment?) {
super.init(processingEnv)
processingEnv?.apply {
mElement = elementUtils
mFiler = filer
mTypeUtils = typeUtils
}
}
override fun process(
annotations: MutableSet<out TypeElement>?,
roundEnv: RoundEnvironment?,
): Boolean {
if (annotations != null && annotations.size != 0) {
val elementsAnnotated = roundEnv?.getElementsAnnotatedWith(BindView::class.java)
elementsAnnotated?.forEach {
// VariableElement用于表示字段、枚举常量、方法或构造函数参数、局部变量、资源变量或异常参数。
val variableElement = it as VariableElement
// TypeElement表示类或接口程序元素
val typeElement = variableElement.enclosingElement as TypeElement
// 将viewInfos实例化
var viewInfos = mBindActivity[typeElement]
if (viewInfos == null) {
viewInfos = hashSetOf()
mBindActivity[typeElement] = viewInfos
}
// 拿到注解的值,这里的值是view xml布局的id
val annotation = variableElement.getAnnotation(BindView::class.java)
val xmlId = annotation.value
viewInfos.add(ViewInfo(variableElement.simpleName.toString(), xmlId))
}
// 生成不同的Activity类,生成路径为...
mBindActivity.keys.forEach {
// 获取要绑定的view名称
val className = it.simpleName.toString()
// 获取绑定View所在类的包名
val packageElement = mElement?.getPackageOf(it) as PackageElement
val packageName = packageElement.qualifiedName.toString()
// 生成的类的名字
val generateCalssName = "$className$\$Processor"
// 开始编写模板代码
val code = StringBuilder()
code.append("package $packageName;\n")
code.append("import com.binyouwei.lib_apt.template.IBindHelper;\n")
code.append("public class $generateCalssName implements IBindHelper {\n")
code.append("\t@Override\n")
code.append("\tpublic void inject(Object target) {\n")
code.append("\t\t$className obj = ($className)target;\n")
mBindActivity[it]?.forEach { viewInfo ->
viewInfo.viewName?.run {
val initView = "\t\tobj.set${substring(0, 1).uppercase()}${substring(1)}(obj.findViewById(${viewInfo.id}));\n"
code.append(initView)
}
}
code.append("\t}\n")
code.append("}")
val createSourceFile = mFiler?.createSourceFile(generateCalssName, it)
val writer = createSourceFile?.openWriter()
writer?.apply {
write(code.toString())
flush()
close()
}
}
return true
}
return false
}
class ViewInfo(var viewName: String, var id: Int)
}
- IBindHelper
用于AutoBind
类调用inject
方法给APT
生成的Processor
类传递类名。
interface IBindHelper {
fun inject(target: Any?)
}
注册类
注册自动生成代码的
Processor
类有两种方式,分别是SPI
与AutoService
。
SPI
SPI
全称Service Provider Interface
,是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件。
在这里,使用SPI
来启用BindViewProcessor
扩展类。由于SPI
机制约定通过在 ClassPath
路径下的 META-INF/services
文件夹查找文件,自动加载文件里所定义的类,因此需要先创建指定文件夹。
在Android Studio
的Terminal
打开项目的模块main
文件夹,输入命令行 mkdir -p resources/META-INF/services
或手动在项目 main
目录下创建resources/META-INF/services
文件夹。
紧接着创建一个名为javax.annotation.processing.Processor
的文件,并在文件里输入继承AbstractProcessor
类的类路径(含包名和类名),在我这里填的就是com.binyouwei.lib_apt.processor.BindViewProcessor
,文件目录如下:
完成上述步骤,执行Build
菜单栏下的Rebuild Project
,就能够在app
模块的项目路径\APT\app\build\generated\source\kapt\debug
目录下生成一个BindViewProcessor
类编写的模板类。
如果嫌操作步骤麻烦,这一步可以使用Google
提供的AutoService
的注解代替,更方便快捷。
在Android
项目中,自定义注解需要引入注解处理器,如果在 Java
文件中使用自定义注解,需要使用 annotationProcessor
引入项目,若在Kotlin
文件中使用自定义注解,则添加 kapt(全称Kotlin Annotation Processing Tool)
插件,并使用 kapt
引入依赖。
apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'
但近期 JetBrains
官方发布的教程显示,kapt
已经进入维护模式,且无开发 kapt
相关功能的计划,推荐我们使用 KSP(Kotlin Symbol Processing API),官方理由则是:与 kapt
相比,使用 KSP
的注释处理器的运行速度最多可以提高 2 倍。
SPI
相关文章👉 Java SPI (Service Provider Interface) 机制详解
AutoService
AutoService
是一个java.util.ServiceLoader
风格服务提供程序的配置/元数据生成器,可用于替换SPI
来注册类。
代替SPI
注册类只有两个步骤:
- 在
build.gradle
添加依赖包。
apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'
dependencies {
implementation 'com.google.auto.service:auto-service:1.0-rc7'
kapt 'com.google.auto.service:auto-service:1.0-rc7'
}
- 使用
@AutoService(Processor.class)
注解绑定BindViewProcessor
类
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.apt_annotation.BindView")
class BindViewProcessor : AbstractProcessor() {
...
}
完成上述步骤并编译即可在项目所在路径\APT\app\build\generated\source\kapt\debug
生成一个类,这个类里面生成的代码即模板代码。
感兴趣可参考以下文章👇
1、AutoService介绍
2、AutoService历史版本
PS:如果使用
SPI
和AutoService
没有正常生成类,都会导致AutoBind
类报异常。
JavaPoet
JavaPoet
是一个用于生成.Java
源文件的Java API
。
JavaPoet
相关类:
类 | 描述 |
---|---|
ParameterSpec | 用于生成参数声明 |
FieldSpec | 用于生成的字段声明 |
AnnotationSpec | 在声明上生成的注释 |
JavaFile | 包含单个顶级类的Java 文件 |
ClassName | 顶级类和成员类的完全限定类名 |
MethodSpec | 用于生成的构造函数或方法声明 |
TypeSpec | 用于生成的类、接口或枚举声明 |
CodeWriter | 将 JavaFile 类转换为既适合人工使用又适合javac 使用的字符串。 |
NameAllocator | 指定Java 标识符名称以避免冲突、关键字和无效字符。要使用,请首先创建一个实例并分配所需的所有名称。通常是用户提供的名称和常量的混合。 |
JavaPoet
的使用是蛮简单的,就这几个类,在它的 Github
也举了简单的例子,对着看就能知道自己应该如何根据自己的需求编写代码。
上面我们使用字符串拼接的方式实现类代码的编写,使用 javapoet
进行修改,只需要使用以下代码替换字符串拼接部分即可。
// 编写方法
val method = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC)
.returns(Void.TYPE)
// 给inject方法添加Override注解
.addAnnotation(Override::class.java)
// 给inject方法添加Object类型的target参数
.addParameter(Object::class.java, "target")
mBindActivity[it]?.forEach { viewInfo ->
// 循环生成findviewbyId代码
method.addStatement("$className obj = ($className)target;")
.addStatement("obj.setText(obj.findViewById(${viewInfo.id}));")
}
// 编写类
val generateCalss = TypeSpec.classBuilder(generateCalssName)
// 添加对类的修饰
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
// 为类添加需要实现的接口
.addSuperinterface(IBindHelper::class.java)
// 为类添加方法
.addMethod(method.build())
.build()
// 新增一个文件并写入类
val javaFile = JavaFile.builder(packageName, generateCalss)
.build()
javaFile.writeTo(mFiler)
使用 kapt
的编写的代码所生成的代码路径是和 java
版本的是不一样的,本示例代码生成的类路径如下:项目路径APTJava\app\build\generated\ap_generated_sources\debug\out\com\binyouwei\apt_java\MainActivity$$Autobind.java
更多
JavaPoet
详情见👉 javapoet
最终效果
使用示例:
class MainActivity : AppCompatActivity() {
@BindView(value = R.id.textView)
var text: TextView ?=null
@BindView(value = R.id.Id)
var Id: TextView ?=null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
AutoBind.inject(this)
text?.text = "APT测试成功"
}
}
使用 APT
前后:
前往下载
Kotlin
、Java
版代码👉 APT-Demo
参考文档
1、Android APT快速教程