Android 编译时注解-初认识
背景
编译时注解越来越多的出现在各大开源框架使用中,比如
square/dagger 依赖注入
类似这样的库在开发和工作中已经越来越多,它们旨在帮助我们在效率为前提的情况下帮助开发者快速开发,节约时间成本。而它们都使用了编译时注解的思想。
正因为如此火热,所以有必要好好学习其中的实现原理,方便解决因为编译时注解导致的问题,同时可将此技术运用到自己的开源库中
思想
编译时注解框架在编写时有相对固定的格式,分包为例
格式相对固定,但是也可以灵活变动,比如讲api
和annotations
结合在一个moudel
里
moudel中
的依赖关系也非常的固定
processors
依赖包有api
-annotations
app
依赖包有api
-annotations
-processors
其中除了app
是Android moudel
以外,其他全部均是Java moudel
annotations
注解
在讲解annotations
注解之前,需要对java和android注解有大致的了解,可以参考我之前的博客
先初始一个HelloWordAtion注解标注Target为ElementType.TYPE
修饰类对象
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface HelloWordAtion {
String value();
}
一般一个注解需要对应一个注解处理器,注解处理器在processors
处理
processors
注解处理器
对应注解的处理器需要继承AbstractProcessor
类,需要复写以下4个方法:
init
init(ProcessingEnvironment processingEnv)
会被注解处理工具调用
param | mean |
---|---|
ProcessingEnvironment | 提供很多有用的工具类Elements, Types 和 Filer |
process
process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
param | mean |
---|---|
annotations | 请求处理的注解类型 |
roundEnv | 有关当前和以前的信息环境 |
@return | 如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们 |
@return | 如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们 |
getSupportedAnnotationTypes
etSupportedAnnotationTypes()
这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称
@return
注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合
getSupportedSourceVersion
指定使用的Java
版本,通常这里返回SourceVersion.latestSupported(),默认返回
SourceVersion.RELEASE_6 `
@return
使用的Java
版本
生成注解处理器
对AbstractProcessor
有了深入的了解,知道核心的初始编译时编写代码的方法及时process
,在process
中我们通过得到传递过来的数据,写入代码,这里先采用打印的方式,简单输出信息,后续会详细讲解如何自己实现 butterknife
功能
public class HelloWordProcessor extends AbstractProcessor {
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
// Filer是个接口,支持通过注解处理器创建新文件
filer = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(HelloWordAtion.class)) {
if (!(element instanceof TypeElement)) {
return false;
}
TypeElement typeElement = (TypeElement) element;
String clsNmae = typeElement.getSimpleName().toString();
String msg = typeElement.getAnnotation(HelloWordAtion.class).value();
System.out.println("clsName--->"+clsNmae+" msg->"+msg);
}
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(HelloWordAtion.class.getCanonicalName());
return annotations;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
到这一步HelloWordAtion
对应的注解处理器已经编写完成,这里简单的打印了HelloWordAtion
注解的class
和注解指定的value
信息
准备工作完成以后,app
触发调用
@HelloWordAtion("hello")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
这里注解注释的类MainActivity
并且指定value
为hello
,到此准备工作就算完成了,这时如果你直接编译或者运行工程的话,是看不到任何输出信息的,这里还要做的一步操作是指定注解处理器的所在,需要做如下操作:
1、在 processors 库的 main 目录下新建 resources 资源文件夹;
2、在 resources文件夹下建立 META-INF/services 目录文件夹;
3、在 META-INF/services 目录文件夹下创建 javax.annotation.process.Processors 文件;
4、在 javax.annotation.process.Processors 文件写入注解处理器的全称,包括包路径;
经历了以上步骤以后方可成功运行,但是实在是太复杂了,博主为了配置这一步也是搞了好久,所以这里推荐使用开源框架AutoService
AutoService
直接在Processors
中依赖
compile 'com.google.auto.service:auto-service:1.0-rc2'
使用
@AutoService(Processor.class)
public class HelloWordProcessor extends AbstractProcessor {
xxxxxxx
}
到这里运行程序便可以成功看到后台的输出信息
需要切换到右下角的Gradle Console
窗口,如果变异不成功可以clean
工程以后重新运行
得到需要的数据,下一步当然是将数据写入到java class
中,也就是题目所言的编译时注解,如何才能写入,这里需要借助Filer
类
Filer
在AbstractProcessor
的init
方法中初始Filer
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
}
到此我们已经有了写入的类的帮助类,还差代码生成逻辑,这里介绍使用javapoet
javapoet
JavaPoet一个是创建 .java 源文件的辅助库,它可以很方便地帮助我们生成需要的.java 源文件,GitHub
上面有非常详细的用法,建议好好阅读相关的使用
processors
依赖:
compile 'com.squareup:javapoet:1.8.0'
综合上述的技术,仿照javapoet
的第一个Example
生成如下代码
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(HelloWordAtion.class)) {
if (!(element instanceof TypeElement)) {
return false;
}
TypeElement typeElement = (TypeElement) element;
String clsNmae = typeElement.getSimpleName().toString();
String msg = typeElement.getAnnotation(HelloWordAtion.class).value();
System.out.println("clsName--->"+clsNmae+" msg->"+msg);
// 创建main方法
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, clsNmae+"-"+msg)
.build();
// 创建HelloWorld类
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
try {
// 生成 com.wzgiceman.viewinjector.HelloWorld.java
JavaFile javaFile = JavaFile.builder("com.wzgiceman.viewinjector", helloWorld)
.addFileComment(" This codes are generated automatically. Do not modify!")
.build();
// 生成文件
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
这里重点讲解process
方法,也就是写入代码的方法体,我们在javapoet
的Example
基础上将输出信息改为HelloWordAtion
注解获取的信息,到处便完全搞定编译时注解的整个流程,clean
以后运行工程,在如下路径下便可看到自动编译生成的HelloWorld
类
到此简单的编译时注解就搞定了,但是编译时注解的自动写入也会导致代码混乱,可能在多次build
编译过程中出现文件冲突的情况,所以这里需要引入android-apt
android-apt
android-apt
能在编译时期去依赖注解处理器并进行工作,但在生成 APK 时不会包含任何遗留无用的文件,辅助 Android Studio
项目的对应目录中存放注解处理器在编译期间生成的文件
依赖使用:
根目录build.gradle
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
app
中
apply plugin: 'com.neenbedankt.android-apt'
apt project(':processors')
这里是apt
替换compile
依赖processors
总结
到此简单的编译时注解就搞定了,接下来的博客中继续扩展,运用掌握的编译时注解和时下主流的butterknife
框架,实现一套自己的自定义注入框架,你会发现原来butterknife
很简单,当然可以自由发散,扩展回到自己的任何开源项目中,替换掉反射提高效率。