ButterKnife框架想必大家都使用过,他是出自JakeWharton的一个开源库,其Github地址ButterKnife;它通过注解的方式来替代Android中繁杂的View操作,比如findViewById,setOnClickListener等,因为是编译时注解的使用,所以对性能的影响很小,本篇文章就通过仿写ButterKnife框架的一些主要功能来加深对编译时注解及注解处理器的使用
在上篇文章Android使用运行时注解+反射仿写EventBus组件通信框架 掌握事件总线通信核心原理中就介绍过注解的概念,这里再挪过来:
注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的上面,用来对这些元素进行标记说明,它本身不会在运行时起什么作用,需要我们编写注解处理器处理这些注解(编译时注解),或者在程序运行时通过反射得到这些注解做出相应的处理(运行时注解)
每个注解都必须使用注解接口@interface进行声明,这实际上会创建一个Java接口,也会编译成一个class文件,注解接口内部的元素声明实际上是方法声明,方法没有参数,没有throws语句,也不能使用泛型
注解又分为标准注解、编译时注解和运行时注解:
标准注解:Java API中默认定义的注解我们称为标准注解,它们定义在java.lang、java.lang.annotation、javax.annotation中;按照使用场景不同又可以分为三类:
编译相关注解:编译相关的注解是供编译期使用的,如@Override:编译器会检查被注解的方法是否真的重写了父类的方法,没有的话编译器会提示错误;@Deprecated:用来修饰任何不再鼓励使用或被弃用的方法
资源相关注解:这个一般用在JavaEE领域,在Android开发中没有用到,比如@Resource:用于Web容器的资源注入,表示单个资源
元注解:这个一般用来定义和实现注解的注解,也就是用来修饰注解的,总共有如下5种:
编译时注解:要定义编译时注解只需在定义注解时使用@Retention(RetentionPolicy.SOURCE)修饰即可,编译时注解结合注解处理器(APT)在编译期完成操作,对性能影响很小;像@Nullable@NonNull这类的注解就是编译时注解;一些开源框架如BufferKnife, ARout、Dagger、Retrofit等都有使用编译时注解
运行时注解:只需在定义注解时使用@Retention(RetentionPolicy.RUNTIME)修饰即可;运行时注解一般和反射配合使用,相比编译时注解,性能较低,但是灵活,实现方便;像@Subscribe@Autowired等都是通过反射API进行操作,otto、EventBus等框架会使用运行时注解
要想实现类似的功能,主要有两步,第一就是创建编译时注解,第二步就是创建注解处理器
编译时注解能够自动处理Java源文件并生成更多源码、配置文件、脚本或其它可能想要生成的东西,这些操作都是由注解处理器完成的
首先新建一个library,将所有的注解定义在这个库里,但是要注意这里是新建Java Library,不是Android Library;因为后面创建的注解处理器Library要用到javax里的api,而且要依赖这个注解Library,所以两者都需要是Java Library
然后新建注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
@IdRes int value();
}
在build.gradle中添加依赖
api 'com.android.support:support-annotations:27.0.2'
@IdRes 是 android support library 中的编译时检查注解,表明注解的值必须是资源 ID;当然还可以定义其它的注解,比如BindString、BindColor等
继续新建一个Java Library,定义注解处理器;这里解释下注解处理器,英文Annotation Processing Tool,也就是我们平时说的APT:它是一种处理注解的工具,对源代码文件进行检测找出其中的注解,然后根据注解生成代码;如果想要自定义的注解处理器能够正常运行,必须通过APT来进行处理,或者说只有通过声明APT后,程序在编译期其自定义注解处理器才能执行
自定义注解处理器实现步骤:
添加依赖 implementation ‘com.google.auto.service:auto-service:1.0-rc3’,加入谷歌注解处理器服务,这是注册注解处理器的第一步
定义自定义注解处理器类并继承AbstractProcessor类 ,重写一系列方法
使用@AutoService注解修饰,完成注册注解处理器,这样编译器工作时才会执行这个注解处理器;其原理是利用了 Google 的 AutoService 为注解处理器自动生成 metadata 文件并将注解处理器jar文件加入构建路径,这样也就不需要再手动创建并更新 META-INF/services/javax.annotation.processing.Processor 文件了
/**
* @Description TODO(注解处理器 帮我们生成文件并且写文件)
* @author Mangoer
* @Date 2019/5/22 22:37
* 自定义注解处理器:
* 1. 继承AbstractProcessor类 并重写process方法
* 2. 使用@AutoService注解修饰 注册注解处理器
* 在编译期间,编译器会定位到Java源文件中的注解(因为有RetentionPolicy.CLASS修饰),注解处理器会对其感兴趣的注解进行处理
* 一个注解处理器只能产生新的源文件,不能修改一个已经存在的源文件
*/
@AutoService(Processor.class)
public class AnotationComplie extends AbstractProcessor {
//创建Java源文件、Class文件以及其它辅助文件的对象
Filer mFiler;
//包含了一些用于操作Element的方法
Elements mElementUtils;
/**
* 初始化方法被注解处理工具调用,并传入参数,这个参数包含了很多有用的工具类
* 比如Elements、Types、Filer等
* @param processingEnv
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
}
/**
* 这个方法很重要,指定这个注解处理器能够处理的注解
* 返回一个Set集合,里面保存我们希望它处理的注解
* @return
*/
@Override
public Set getSupportedAnnotationTypes() {
Set type = new LinkedHashSet<>();
type.add(BindView.class.getCanonicalName());
return type;
}
/**
* 指定注解处理器使用的Java版本,通常返回SourceVersion.latestSupported()即可
* 也可以指定支持某个版本的Java,比如SourceVersion。RELEASE_6
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 实现注解处理器的具体业务逻辑
* 这里我们就需要编写findViewById等代码
* @param annotations
* @param roundEnv
* @return
*/
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}
同时在build.gradle文件添加一句
//注册我们的注解处理器,告诉虚拟机我们这个module里面的自定义注解处理器
implementation 'com.google.auto.service:auto-service:1.0-rc3'
这里面最重要的方法就是process,我们的逻辑就在这里面编写,如下
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
/**
* 要知道我们的Java文件都是结构化的数据,最外层就是类声明,里面是全局变量,然后就是方法
* 这样每一个结点都对应一个标签,比如
* 类标签---------TypeElement
* 成员变量标签---VariableElement
* 方法标签-------ExecutableElement
*
* 所以这里拿到的Set集合就是程序中所有使用到BindView注解的结点对应的标签
*/
Set extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
/**
* 把所有使用到BindView的数据结构化
* key-----类名
* value---每个类里使用到BindView的成员变量的集合
*/
Map> eleMap = new HashMap<>();
/**
* 遍历所有使用到BindView注解的结点,也就是成员变量
* 将其与所在类一一对应
*/
for (Element element : elements) {
VariableElement variableElement = (VariableElement) element;
//通过成员变量结点获取上一个结点,也就是拿到类结点,再拿到类名
String className = variableElement.getEnclosingElement().getSimpleName().toString();
List elementList = eleMap.get(className);
if (elementList == null) {
elementList = new ArrayList<>();
eleMap.put(className,elementList);
}
elementList.add(variableElement);
}
/**
* 接下来就是重点了,开始写Java文件了
* Java文件是结构化数据
*/
Iterator iterator = eleMap.keySet().iterator();
//写文件对象
Writer writer = null;
while (iterator.hasNext()) {
//拿到每个类的所有成员变量
String className = iterator.next();
List elementList = eleMap.get(className);
//拿到包名,主要是通过成员变量结点的上一个结点,也就是类结点,获取它的包信息
String packageName = mElementUtils.getPackageOf(elementList.get(0).getEnclosingElement()).toString();
/**
* 创建Java文件
* createSourceFile---创建Java文件
* createClassFile----创建Class文件
* createResource-----创建资源文件
*/
try {
JavaFileObject fileObject = mFiler.createSourceFile(packageName+"."+className+"$ViewBinder");
writer = fileObject.openWriter();
//写类的包名
writer.write("package " + packageName + ";\n");
writer.write("\n");
// 写导入的类
writer.write("import " + packageName + ".ViewBinder" + ";\n");
writer.write("\n");
//定义类
writer.write("public class " + className + "$ViewBinder implements ViewBinder<" + packageName + "." + className + ">{" + "\n");
//定义bind方法 接收参数
writer.write(" public void bind(" + packageName + "." + className + " target){" + "\n");
//写findViewById
for (VariableElement variableElement : elementList) {
//获取变量名
String name = variableElement.getSimpleName().toString();
//获取变量类型
TypeMirror typeMirror = variableElement.asType();
//获取控件id
BindView annotation = variableElement.getAnnotation(BindView.class);
int id = annotation.value();
writer.write(" target." + name + " = (" + typeMirror + ")target.findViewById(" + id + ");"+"\n");
}
writer.write(" }\n");
writer.write("}");
} catch (IOException e) {
e.printStackTrace();
}finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return false;
}
通过这个方法,你应该知道两个东西
结构体语言:Java就是一种结构体语言,Java文件都是结构化的数据,由一个个结点Element组成的,比如类声明结点,成员变量声明结点,方法声明结点;如果你写过Html,就很容易理解结构体语言了;这里需要获取整个工程所有使用到我们定义的BindView注解的结点,然后将所有结点按类分开保存;接下来就是写每个Java文件了,把属于同一个类的结点写到同一个Java文件中;最后就是写findViewById了,当然了还可以写其它的
其实注解处理器重点就是帮我们在编译期写Java文件,当然了还可以写其它文件了;这里其实就是相当于我们自己写个Java类,然后在里面把findViewById等操作写在里面,到运行时直接用这里面的代码,就不需要我们在Activity里写;所以我们通过编译时注解和注解处理器来写Java文件,就不需要我们写这些代码
都说了是在编译期就帮我们把代码写好了,那是不是真的呢?先在几个Activity使用注解BindView
然后Build一下工程,在如下图目录就会生成两个文件
可以看到编译完成后,生成了这么两个Java文件,里面帮我们写好了findViewById操作,这样我们只要调用这个bind方法就可以了
接下来再新建一个Module,这是一个Android Library,因为它要用到Android里的api
新建一个ViewBinder接口
public interface ViewBinder{
void bind(T target);
}
这个就接口就是上面用到的,它的目的就是方便给Activity调用的
再新建一个工具类
public class MangoKnife {
public static void bind(Activity activity){
String complieName = activity.getClass().getName()+"$ViewBinder";
try {
ViewBinder viewBinder = (ViewBinder) Class.forName(complieName).newInstance();
viewBinder.bind(activity);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
这个方法就是给Activity或者Fragment使用的,它的逻辑是通过反射实例化出刚才生成的Java类,因为它们实现了ViewBinder接口,所以直接将其强转成这个类型,调用其bind方法
使用编译时注解技术可以帮我们省却很多工作,极大的提高开发效率,由于不涉及到反射就不会影响运行时性能,同时让代码更加简洁优美,值得我们去掌握
有疑问的朋友欢迎下方留言
觉得不错的朋友就点个赞吧
本文代码可从MangoBus下载