编译时注解
运行时注解和编译时注解,两种注解方式对性能的影响是不一样的。之前看到相关资料,都说对于注解的优化,都用的是编译时注解进行性能的提升。自己在使用的时候也查阅各种博客、第三方库的代码,对于编译时注解的实现原理进行理解以及记录。
编译时注解框架基本构成
- compiler
- api
- annotation
compiler
这部分主要是框架所使用的注解处理器(Annotation Processer),用于在编译时扫描和处理注解。也可以自定义,并且处理自己的注解逻辑。
自定义注解器需要继承 AbstractProcessor
,并且实现四个方法:
- init
- process
- getSupportedAnnotationTypes
- getSupportedSourceVersion
init 方法
处理器的初始化
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//错误处理工具
mMessager = processingEnvironment.getMessager();
//Filer可以创建文件
mFileUtils = processingEnvironment.getFiler();
//Element代表程序的元素,例如包、类、方法。
mElementUtils = processingEnvironment.getElementUtils();
//处理TypeMirror的工具类,用于取类信息
mTypeUtils = env.getTypeUtils();
}
process
/**
* 处理器实际处理逻辑入口
* @param set
* @param roundEnvironment 所有注解的集合
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mMessager.printMessage(Diagnostic.Kind.NOTE,"Start BindProcesser process method!!");
//获取注解的元素变量
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
//循环处理注解的每一个元素,并且放入一个 map 中
for(Element element : elements){
//校验元素是否为 VariableElement
if(!(element instanceof VariableElement)){
return false;
}
//转换变量类型
VariableElement variableElement = (VariableElement)element;
//修饰变量所在的类
TypeElement typeElement = (TypeElement)variableElement.getEnclosingElement();
//使用类的全路径作为key
String qulifiedName = typeElement.getQualifiedName().toString();
//获取 map 中是都已经有相关的代理信息
ProxyInfo proxyInfo = mProxyInfoMap.get(qulifiedName);
if(proxyInfo == null){
proxyInfo = new ProxyInfo(typeElement,mElementUtils);
mProxyInfoMap.put(qulifiedName,proxyInfo);
}
//获取注解
BindView annotation = variableElement.getAnnotation(BindView.class);
//注解上的控件ID
int id = annotation.value();
proxyInfo.injectVariables.put(id,variableElement);
//第二步骤: 遍历Map生成代理类
for(String key: mProxyInfoMap.keySet()){
ProxyInfo proxyInfo2 = mProxyInfoMap.get(key);
try {
//创建文件对象
JavaFileObject soureFile = mFileUtils.createSourceFile(
proxyInfo2.getProxyClassFullName(), //文件名,全路径
proxyInfo2.getTypeElement());
//创建写入对象
Writer writer = soureFile.openWriter();
//写入内容
writer.write(proxyInfo2.generateJavaCode());
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return true;
}
以上代码是参照文章</br>
https://blog.csdn.net/niubitianping/article/details/78492054
处理器的逻辑:
遍历得到源码中,需要解析的元素列表。</br>
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
</br>
Processor的过程中,回遍历所有的java源码,查找到相关的元素 Element。代码的每一个部分就相当于一个 Element ,每个Element代表一个静态的、语言级别的构件。</br>
PackageElement -- 包 </br>
TypeElement -- 类 </br>
VariableElement -- 变量 </br>
ExecuteableElement -- 方法 </br>-
判断元素是否可见和符合要求。</br>
获取所有的Element之后,则开始校验判断是否可用或者是符合自己的业务逻辑。</br>//校验元素是否为 VariableElement if(!(element instanceof VariableElement)){ return false; } //或者通过如下api校验元素是否可用 SuperficialValidation.validateElement(element); // 或者检查元素是否是一个类 if (element.getKind() != ElementKind.CLASS) { ... }
-
组织数据结构得到输出类参数。
例如:</br>//修饰变量所在的类 TypeElement typeElement = (TypeElement)variableElement.getEnclosingElement(); //使用类的全路径作为key String qulifiedName = typeElement.getQualifiedName().toString(); //获取 map 中是都已经有相关的代理信息 ProxyInfo proxyInfo = mProxyInfoMap.get(qulifiedName); if(proxyInfo == null){ proxyInfo = new ProxyInfo(typeElement,mElementUtils); mProxyInfoMap.put(qulifiedName,proxyInfo); } //获取注解 BindView annotation = variableElement.getAnnotation(BindView.class); //注解上的控件ID int id = annotation.value(); proxyInfo.injectVariables.put(id,variableElement); //第二步骤: 遍历Map生成代理类 for(String key: mProxyInfoMap.keySet()){ ProxyInfo proxyInfo2 = mProxyInfoMap.get(key);
-
输入生成java文件。
//第二步骤: 遍历Map生成代理类 for(String key: mProxyInfoMap.keySet()){ ProxyInfo proxyInfo2 = mProxyInfoMap.get(key); try { //创建文件对象 JavaFileObject soureFile = mFileUtils.createSourceFile( proxyInfo2.getProxyClassFullName(), //文件名,全路径 proxyInfo2.getTypeElement()); //创建写入对象 Writer writer = soureFile.openWriter(); //写入内容 writer.write(proxyInfo2.generateJavaCode()); writer.flush(); writer.close(); } catch (IOException e) { e.printStackTrace(); } }
Java代码生成有两种方式:</br>
一种是 使用 Filer 进行生成,如下代码:</br>/** * 生成java文件代码 * @return */ public String generateJavaCode() { StringBuilder builder = new StringBuilder(); builder.append("// Generated code. Do not modify!\n"); builder.append("package ").append(packageName).append(";\n\n"); //注意,这个ImPort的包路径,是api的包路径 builder.append("import com.example.qhh_api.*;\n"); builder.append('\n'); builder.append("public class ").append(proxyClassName).append(" implements " + ProxyInfo.PROXY + "<" + mTypeElement.getQualifiedName() + ">"); builder.append(" {\n"); generateMethods(builder); builder.append('\n'); builder.append("}\n"); return builder.toString(); } /** * 生成方法 * @param builder */ private void generateMethods(StringBuilder builder) { builder.append("@Override\n "); builder.append("public void inject(" + mTypeElement.getQualifiedName() + " host, Object source ) {\n"); for (int id : injectVariables.keySet()) { VariableElement element = injectVariables.get(id); String name = element.getSimpleName().toString(); String type = element.asType().toString(); builder.append(" if(source instanceof android.app.Activity){\n"); builder.append("host." + name).append(" = "); builder.append("(" + type + ")(((android.app.Activity)source).findViewById( " + id + "));\n"); builder.append("\n}else{\n"); builder.append("host." + name).append(" = "); builder.append("(" + type + ")(((android.view.View)source).findViewById( " + id + "));\n"); builder.append("\n}"); } builder.append(" }\n"); }
第二种方式,使用 JavaPoet 库,导包方式:</br>
compile 'com.squareup:javapoet:1.9.0' </br>
最新 'com.squareup:javapoet:1.11.1'注意:JavaPoet 也是必须在 Java Library 中使用,因为Javax的核心包在 Android module 和 Library中都不存在。Android Library 调用 Java Library中的方法包含 javax 库文件的会报错。
MethodSpec methodSpec = MethodSpec.methodBuilder("inject") .addModifiers(Modifier.PRIVATE) .returns(void.class) .addParameter(String.class, "id") .addStatement("String view = id") .build(); TypeSpec typeSpec = TypeSpec.classBuilder("ViewInject") .addModifiers(Modifier.PUBLIC) .addMethod(methodSpec) .build(); JavaFile javaFile = JavaFile.builder("com.sensetime.test", typeSpec) .build(); try { javaFile.writeTo(mFileUtils); } catch (IOException e) { e.printStackTrace(); }
JavaPoet 的使用可以参考:</br>
https://github.com/square/javapoet </br>
https://juejin.im/post/584d4b5b0ce463005c5dc444 错误处理。
在工程中生成 compiler(Annotation Processer),需要生成 Java Library,而不是 Android Library 。
总结
主要是通过一些网上的Demo,以及ButterKnife的源码,来理解编译时注解框架的工作原理。简单的使用编译时注解,完成对项目的一些解耦探索。</br>
在使用依赖的时候,出现在Activity中输入自定义的注解,无法自动提示,只能输入全称的问题。找了好多好多参考,最终自己输入全称实现,很尴尬。
小小的demo : https://github.com/qinhaihang/AnnotationProcesserDemo