一、注解
在使用Java语言开发的过程中,我们会经常看到各种各样的注解,@Override(表示方法的重写),@Deprecated(标记过时的元素 方法,类或属性),@LayoutRes(表示的是布局资源),@IdRes(表示的是ID资源),@DrawableRes(表示的图片资源)等等,另外在一些第三方库中,如Butterknife,EventBus,Retrofit中也会使用了很多自定义的注解;
那到底什么是注解?
注解本身是没有什么意义的,单独的注解就是一种注释或者说是一种标记,他要结合APT(编译时注解处理),字节码插桩,反射等这些技术使用才有意义;
使用注解的主要作用是什么?
(1)为了简化代码,减少模板代码的编写;
注解和APT结合,如Butterknife,我们使用一个@BindView的注解,就可以不用去写findViewById()的重复代码,如Retrofit中大量使用注解,我们可以通过简单的注解@POST,@GET,@Header就可以简洁方便的设置HTTP请求的方式,请求头,请求参数等;
所以注解的一个很大作用就是简化代码,减少模板代码的编写
(2)编写代码时可以进行语法的检查
像@LayoutRes(表示的是布局资源),@IdRes(表示的是ID资源),@DrawableRes(表示的图片资源),当我们的方法的参数中使用了注解,在编写代码时,传入的参数不是布局,ID,图片类型的整形数据时,是会立即有提示的;
(3)就是一个注释作用
@Override(表示方法的重写),@Deprecated(标记过时的元素 方法,类或属性)
注解的本质
自定义一个注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface MQBindView {
int value();
}
注解对应的字节码
public abstract @interface com/example/annotationmodule/MQBindView implements java/lang/annotation/Annotation {
@Ljava/lang/annotation/Target;(value={Ljava/lang/annotation/ElementType;.FIELD})
@Ljava/lang/annotation/Retention;(value=Ljava/lang/annotation/RetentionPolicy;.SOURCE)
// access flags 0x401
public abstract value()I
}
查看注解的字节码可以看到,注解本质上就是一个实现了Annotation接口的接口
元注解
元注解是对注解的注解
(1)@Target
表明注解可以应用的Java元素类型
ElementType.TYPE 应用于类,接口,枚举
ElementType.FIELD 应用于属性(包括枚举中的变量)
ElementType.METHOD 应用于方法
ElementType.PARAMETER 应用于方法中的形参
ElementType.CONSTRUCTOR 应用于构造方法
ElementType.LOCAL_VARIABLE 应用于局部变量
ElementType.ANNOTATION_TYPE 应用于注解类型
ElementType.PACKAGE 应用于包
(2)@Retention
表明注解的生命周期
RetentionPolicy.SOURCE 即在源文件中保留,编译之后就没有了;
RetentionPolicy.CLASS 保留到编译后的字节码文件中,但是在类加载进虚拟机时就会被忽略了;
RetentionPolicy.RUNTIME 保留到程序运行时,在类加载到虚拟机中时,仍然保留,可以通过反射获取;
(3)@Document
使用了@Document注解的注解,表示该标记的元素可以被JavaDoc或类似工具文档化
(4)Inherited
使用Inherited注解标记的注解,所标记的类的子类也会拥有该注解
自定义注解
自定义注解,要使用@Interface关键字修饰
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface MQBindView {
int value();
}
二、APT
1.APT
上面对 什么是注解,以及注解在我们平时写代码中的作用有了了解和总结,下面我们就开始学习一下APT,编译时注解处理技术;
APT即Annotations Processing Tool,注解处理工具,APT就是javac的一个工具,在程序编译时,会对我们代码中的所有注解进行扫描和处理,注解处理工具最终生成处理注解逻辑的.java源代码文件,减少模板代码的编写,提高编码效率,使得代码更为简介,可读性高
2.使用APT的步骤
(1)创建自定义注解
(2)创建注解处理器 继承自AbstractProcessor,生成处理注解逻辑的.java文件,封装一个供外部调用的API接口,也就是调用第二步中生成的注解处理逻辑文件中的方法,实现逻辑处理
(3)然后在需要使用的Module中依赖注解,使用自定义注解处理器,然后调用API接口即可
下面我们以手写实现一个简易的Butterknife为例,来实际使用一下APT, 我们这里主要以通过注解的方式实现findViewById()的操作;
2.1 创建自定义注解
我们首先创建一个Java Library Module,名称为AnnotationModule,在该Module下定义一个注解@MQBindView
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface MQBindView {
int value();
}
2.2 自定义注解处理器
创建一个Java Library Module,名称为APTModule,定义一个类继承自AbstractProcessor;
这里自定义处理器里面会用到我们自定义的注解,所以这个Module的build.gradle里面要依赖AnnotationModule
dependencies {
implementation project(path: ':AnnotationModule')
}
public class MQProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
}
//重写getSupportedAnnotationTypes方法,添加支持处理的注解类型
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(MQBindView.class.getCanonicalName());
return supportTypes;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
自定义注解处理器一定要重写getSupportedAnnotationTypes方法,添加需要处理的注解类型
在编写注解处理逻辑之前,这里我先对Processor中的一些基本概念进行一下了解;
我们的app Module中如果依赖了这个APTModule,当我们如果build(编译)项目的时候,这个Processor的process方法就会被执行;
当我们想要在这个Processor中添加一些打印信息,我们需要用Messager(javax.annotation.processing.Messager)去打印,打印出来的内容,在build的时候,会在下方的build视图中打印出来,这个build视图,正常会显示的是编译的过程信息,我们使用Messager打印的信息,也会在这里打印出来;
public class MQProcessor extends AbstractProcessor {
Messager mMessager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mMessager = processingEnv.getMessager();
mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->init");
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->process");
}
}
Element
Element代表程序的一个元素,这个元素可以是:包、类/接口、属性变量、方法/方法形参、泛型参数,Element是java-apt( 编译时注解处理器)技术的基础,在编译期间可以获取元素的各类信息,结合APT技术动态生成代码实现相应的逻辑处理;
Element接口中的一些方法:
getEnclosingElement()
获取包含该Element的父Element
public class MainActivity extends AppCompatActivity {
@MQBindView(R.id.tv_activity_main)
TextView mTextView;
}
这里,当我们拿到使用了@MQBindView这个注解的Element(mTextView),然后通过getEnclosingElement()方法就能获取到父Element,这里父Element就是MainActivity
process方法中,有一个参数RoundEnvironment,通过这个参数可以获取包含特定注解的被注解元素
Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(MQBindView.class);
init方法中有一个ProcessingEnvironment参数,通过这个参数可以获取元素所在的包
String packageName = mProcessingEnv.getElementUtils().getPackageOf(element).toString();
Filer
Filer是文件生成器,可以用来生成Java源文件等
JavaFileObject sourceFile = mFiler.createSourceFile(文件名);
这里文件名,要是创建的类的全类名
JavaFileObject sourceFile = mFiler.createSourceFile(
packageName + "." + activityName + "_ViewBinding"
);
这样创建文件之后,编译项目之后,会在app/build/generated/ap_generated_sources/debug/out/应用包名/下生成这个Java文件
Writer
我们创建Java源文件对象 JavaFileObject之后可以通过openWriter()方法获取文件的Writer对象,然后就可以通过Writer对象往文件里面写内容
而我们要实现Butterknife一样的通过注解的形式,帮我们实现findViewById()的功能,我们查看Butterknifer就知道实际上Butterknife就是使用了APT技术,在编译时给每一个使用了@BindView注解的类都生成了一个对应的Java源文件,名字为使用了注解的类的类名加上 _ViewBinding,比如我们的MainActivity使用了注解@BindView,则编译之后会生成MainActivity_ViewBinding这个Java源文件,生成的Java源文件就在app/build/generated/ap_generated_sources/debug/out/应用包名/ 这个目录下,也即新建了一个MainActivity_ViewBinding类,这个类的构造方法里面就会对MainActivtiy里面的所有使用了@BindView的View成员进行findViewById的操作,如下图所示
ButterKnife通过APT生成的Java源文件
Buterknife.bind(Activity)这个方法的实现如下
查看Butterknife的源码可知,Butterknife.bind(Activity)的实现,就是利用反射通过Class获取生成的Java源文件中定义的类的Class对象,然后获取Class的构造方法,通过反射,创建生成的Java类的对象,从而触发构造方法,触发findViewById相关代码;
了解了Butterknife的实现原理,那我们自己就可以照着这个逻辑去实现;
我们在自己自定义的注解处理器的process方法中,实现给每一个使用了@MQBindView的注解的类都生成一个对应的Java源文件,Java 源文件中的内容,为定义一个对应的类,类中的构造方法需要一个Activtiy参数,构造方法中的内容为给类中每一个使用了@BindView注解的成员进行findViewById的操作;
public class MQProcessor extends AbstractProcessor {
private static final String TAG = "MQProcessor";
Filer mFiler;
ProcessingEnvironment mProcessingEnv;
Messager mMessager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mProcessingEnv = processingEnv;
mMessager = processingEnv.getMessager();
mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->init");
}
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(MQBindView.class.getCanonicalName());
return supportTypes;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->process");
//我们的源代码中所有使用了@MQBindView注解的元素
Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(MQBindView.class);
//定义一个Map集合,以每一个Activity类名为键,这个Activtiy类中使用了@MQBindViwe注解的元素列表为值
Map<String, List<VariableElement>> map = new HashMap<>();
for (Element element : elementsAnnotatedWith) {
VariableElement variableElement = (VariableElement) element;
//Activity的名字
String activityName = variableElement.getEnclosingElement().getSimpleName().toString();
List<VariableElement> variableElements;
if (map.get(activityName) != null) {
variableElements = map.get(activityName);
variableElements.add(variableElement);
} else {
variableElements = new ArrayList<>();
variableElements.add(variableElement);
map.put(activityName, variableElements);
}
}
Writer writer = null;
//给每一个使用了@MQVindView注解的类(Activity/Fragment等)都生成对应的Java源文件
for (Map.Entry<String, List<VariableElement>> stringListEntry : map.entrySet()) {
List<VariableElement> variableElementList = stringListEntry.getValue();
TypeElement enclosingTypeElement = (TypeElement) variableElementList.get(0).getEnclosingElement();
String packageName =
mProcessingEnv.getElementUtils().getPackageOf(enclosingTypeElement).toString();
String activityName = variableElementList.get(0).getEnclosingElement().getSimpleName().toString();
try {
mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->process:" + mFiler);
JavaFileObject sourceFile = mFiler.createSourceFile(
packageName + "." + activityName + "_ViewBinding"
);
/**
* Java源文件里的内容为定义一个类,类名就为使用了@BindView的注解的Activity的类名加_ViewBinding
* 然后构造方法有一个Activity类型的target参数,构造方法里面给这个Activity中使用了@BindView注解 的View成员进行dinfViewByid操作
*
*/
writer = sourceFile.openWriter();
writer.write("package " + packageName + ";\n");
writer.write("import android.view.View;\n");
writer.write("public class " + activityName + "_ViewBinding {\n");
writer.write("public " + activityName + "_ViewBinding (" + activityName + " target){\n");
writer.write("View decorView = target.getWindow().getDecorView();\n");
for (VariableElement variableElement : variableElementList) {
String variableName = variableElement.getSimpleName().toString();
int value = variableElement.getAnnotation(MQBindView.class).value();
writer.write("target." + variableName + "=decorView.findViewById(" + value + ");\n");
}
writer.write("}\n");
writer.write("}\n");
try {
mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->process:" + writer);
//最终Writer这个写对象,要关闭,否则内容是不会写进创建的Java源文件中的
writer.close();
} catch (IOException exception) {
exception.printStackTrace();
}
} catch (Exception exception) {
exception.printStackTrace();
}
}
mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->process:end");
return false;
}
}
2.3 配置注解处理器
自定义处理器代码编写好之后,还有一个非常重要的步骤,就是配置注解处理器
一共有两种方式:
第一种:
在自定义注解处理器所在的Module,APTModule的src/main目录下新建一个 resources/META-INF/services
三级目录,然后在services目录下新建一个名字为javax.annotation.processing.Processor的文件
文件里面的内容,写上自定义处理器的全类名即可
第二种:
通过依赖google的auto-services库,帮我们自动生成META-INF/services/javax.annotation.processing.Processor 文件;
在自定义处理器所在的Module,APTModule的build.gradle添加auto-services依赖
dependencies {
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
implementation project(path: ':AnnotationModule')
}
然后在我们编写的MQProcessor上方使用@AutoService注解
然后编译代码,我们可以看到在APTModule的build/classess/java/main/下会生成 META-INF/services/javax.annotation.processing.Processor文件,文件里面的内容就是com.example.aptmodule.MQProcessor
所以也就成功配置了依赖处理器,然后我们build一下项目,查看app/build/generated/ap_generated_sources/debug/out/应用包名/下成功生成了对应的Java源文件,表示注解处理器生效了
2.4 定义一个使用入口,给使用者调用
我们这里定义了一个MQButterKnife类,提供了一个静态方法bind(Activity), bind 方法里面就是通过反射创建Activity对应生成的 Activity_ViewBinding对象触发findViewById操作
public class MQButterKnife {
private static final String TAG = "MQButterKnife";
public static void bind(Activity activity) {
String viewBindingClassName =
activity.getClass().getCanonicalName() + "_ViewBinding";
Log.e(TAG, "bind:" + viewBindingClassName);
try {
Class<?> aClass = Class.forName(viewBindingClassName);
Log.e(TAG, "bind: " + aClass);
Constructor<?> constructor = aClass.getConstructor(activity.getClass());
Object o = constructor.newInstance(activity);
Log.e(TAG, "bind: " + o);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.5 使用
在app主Module里面先依赖AnnotationModule,以及使用APTModule注解模块,在主Module的build.gradle里面添加如下,注意自定义处理器模板的依赖方式是通过annotationProcessor
dependencies {
implementation project(path: ':AnnotationModule')
annotationProcessor project(':APTModule')
}
然后在我们的Activty中去使用我们自定义的MQButterknife,实现View的findViewById操作
public class MainActivity extends AppCompatActivity {
@MQBindView(R.id.tv_activity_main)
TextView mTextView;
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MQButterKnife.bind(this);
mTextView.setText("测试");
}
}
运行项目,mTextView成功显示 “测试”,说明我们的简单版本Bunnerknife成功实现了
这样我们就成功的在Android项目中利用APT编译时注解处理技术实现了一个简单版本的Butterknife,也对APT技术的使用流程有了一定的了解;
欢迎扫码订阅公众号,公众号主要分享原创或转载移动开发技术文章,和广大移动开发者一起学习成长!