转载请注明出处:https://blog.csdn.net/binbinqq86/article/details/79666374
1、概述
在上一篇文章(一步一步带你轻松打造自己的ButterKnife注解框架(上))中给大家讲解了注解的基本知识和怎么去写一个自己的运行时注解,而今天就要继续带大家来看一下,怎么去写一个自己的编译时注解,降低在大量使用注解的时候,里面的反射对性能的影响。
2、项目框架
类似Arouter、ButterKnife等,一般此类框架需要多个module,下面是本文的框架:
- module_annotation:用于存放注解等,Java模块
- module_compiler:用于编写注解处理器,Java模块
- module_api:用于给用户提供使用的API,本例为Andriod模块
- module_app:示例,本例为Andriod模块
当然如上只是本案例采用的模块方案,命名和具体模块方案也并没有一个标准的统一,比如有人嫌模块太多,就把上面三个模块放在一起,这样也是可以的,只不过分开写的话更清楚明了,职责划分也更加明确,便于以后的维护和修改以及扩展。并且compiler模块只在编译期间用到,最终打包的时候并不需要,所以是不必打包到程序中的。
对于module间的依赖关系如下:
compiler依赖annotation
api依赖compiler
app依赖api
3、注解模块的实现
注解模块基本上跟上篇文章中讲到的差不多,看代码:
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface FindId {
int value();
}
/**
* @auther tb
* @time 2018/3/28 下午4:10
* @desc
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
int[] value();
}
两个注解分别代表了id、布局的查找和view的点击事件,不同的是我们的保留策略由Runtime变为了CLASS,也就是编译时注解。
4、注解的处理(processor)
定义完注解后,就该实现这个注解的处理了,这块内容要比上文讲的运行时注解麻烦多了,不过掌握以后也就明白其中原理,写起来就很顺手了。首先我们要引入谷歌的一个包
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api 'com.google.auto.service:auto-service:1.0-rc2'
api project(':module_annotation')
api 'com.squareup:javapoet:1.9.0'
}
auto-service库可以帮我们去生成META-INF等信息。
下面来看具体注解处理器怎么去写,首先就是要继承AbstractProcessor,在类的顶部加入注解:@AutoService(Processor.class),主要是用来自动生成 META-INF 信息。(注意:Processor千万不要写错为Process,笔者当初就是写错了,导致郁闷了好多天怎么都不生成java代码,惭愧~)
然后覆盖其中的几个方法,分别是:
- public synchronized void init(ProcessingEnvironment processingEnvironment)
- public SourceVersion getSupportedSourceVersion()
- public Set getSupportedAnnotationTypes()
- public boolean process(Set set, RoundEnvironment roundEnvironment)
其中init方法用来初始化一些变量信息,如下:
- Filer mFileUtils; 跟文件相关的辅助类,生成JavaSourceCode.
- Elements mElementUtils;跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
- Messager mMessager;跟日志相关的辅助类。
看代码:
private Filer mFileUtils;
private Elements mElementUtils;
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mFileUtils = processingEnvironment.getFiler();
mElementUtils = processingEnvironment.getElementUtils();
messager = processingEnvironment.getMessager();
}
getSupportedSourceVersion主要是声明支持的Java源码版本
getSupportedAnnotationTypes主要是声明 Processor 处理的注解,注意这是一个数组,表示可以处理多个注解,如果少加了,那么这个注解肯定不会生效的,楼主在写的时候就犯了这个错误,导致排查半天。
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_8;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotationTypes = new LinkedHashSet<String>();
annotationTypes.add(FindId.class.getCanonicalName());
annotationTypes.add(OnClick.class.getCanonicalName());
return annotationTypes;
}
而这两个方法也可以换一种方式去写(采用注解):
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"example.tb.com.module_annotation.FindId"})
下面就是我们最后一个方法了,也是今天的主角,所有的注解处理都是在这里完成的。process方法的实现需要分为两个步骤走:
- 需要生成的类的所有信息的收集
- 根据收集的信息生成具体的java代码
首先我们声明一个map,用来存放收集的所有使用我们的注解的每一个类的所有信息:
/**
* 一个需要生成的类的集合(key为类的全名,value为该类所有相关的需要的信息)
*/
private Map<String, ProxyInfo2> mProxyMap = new HashMap<String, ProxyInfo2>();
我们以类的全名为key,类的相关所有信息为value来进行保存。ProxyInfo2这个类里面声明的生成java代码所需要的信息:
/**
* @auther tb
* @time 2018/3/27 下午2:44
* @desc 对应需要生成某个类的全部相关信息
*/
public class ProxyInfo2 {
/**
* 类
*/
public TypeElement typeElement;
/**
* 类注解的值(布局id)
*/
public int value;
public String packageName;
/**
* key为id,也就是成员变量注解的值,value为对应的成员变量
*/
public Map<Integer, VariableElement> mInjectElements = new HashMap<>();
/**
* key为id,也就是方法注解的值,value为对应的方法
*/
public Map<Integer, ExecutableElement> mInjectMethods = new HashMap<>();
/**
* 采用类名方式不能被混淆(否则编译阶段跟运行阶段,该字符串会不一样),或者采用字符串方式
*/
public static final String PROXY = "TA";
public static final String ClassSuffix = "_" + PROXY;
public String getProxyClassFullName() {
return typeElement.getQualifiedName().toString() + ClassSuffix;
}
public String getClassName() {
return typeElement.getSimpleName().toString() + ClassSuffix;
}
//......
}
生成一个类,我们大致需要这些信息就够了。下面看看怎么去收集:
/**
* 收集所需生成类的信息
*
* @param roundEnvironment
*/
private void collectionInfo(RoundEnvironment roundEnvironment) {
//process可能会多次调用,避免生成重复的代理类
mProxyMap.clear();
//获得被该注解声明的类和变量
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(FindId.class);
//收集信息
for (Element element : elements) {
if (element.getKind() == ElementKind.CLASS) {
//获取注解的值
TypeElement typeElement = (TypeElement) element;
//类的完整路径
String qualifiedName = typeElement.getQualifiedName().toString();
/*类名*/
String clsName = typeElement.getSimpleName().toString();
/*获取包名*/
String packageName = mElementUtils.getPackageOf(typeElement).getQualifiedName().toString();
FindId findId = element.getAnnotation(FindId.class);
if (findId != null) {
int value = findId.value();
//处理类注解
ProxyInfo2 proxyInfo = mProxyMap.get(qualifiedName);
if (proxyInfo == null) {
proxyInfo = new ProxyInfo2();
mProxyMap.put(qualifiedName, proxyInfo);
}
proxyInfo.value = value;
proxyInfo.typeElement = typeElement;
proxyInfo.packageName = packageName;
}
} else if (element.getKind() == ElementKind.FIELD) {
//获取注解的值
FindId findId = element.getAnnotation(FindId.class);
if (findId != null) {
int value = findId.value();
//处理成员变量注解
VariableElement variableElement = (VariableElement) element;
//这里先要获取上层封装类型,然后强转为TypeElement
String qualifiedName = ((TypeElement) element.getEnclosingElement()).getQualifiedName().toString();
ProxyInfo2 proxyInfo = mProxyMap.get(qualifiedName);
if (proxyInfo == null) {
proxyInfo = new ProxyInfo2();
mProxyMap.put(qualifiedName, proxyInfo);
}
proxyInfo.mInjectElements.put(value, variableElement);
}
} else {
continue;
}
}
//获得被该注解声明的方法
Set<? extends Element> elementsMethod = roundEnvironment.getElementsAnnotatedWith(OnClick.class);
for (Element element : elementsMethod) {
if (element.getKind() == ElementKind.METHOD) {
//获取注解的值
OnClick onClick = element.getAnnotation(OnClick.class);
if (onClick != null) {
int[] value = onClick.value();
if (value != null && value.length > 0) {
for (int i = 0; i < value.length; i++) {
ExecutableElement executableElement = (ExecutableElement) element;
//这里先要获取上层封装类型,然后强转为TypeElement
String qualifiedName = ((TypeElement) element.getEnclosingElement()).getQualifiedName().toString();
ProxyInfo2 proxyInfo = mProxyMap.get(qualifiedName);
if (proxyInfo == null) {
proxyInfo = new ProxyInfo2();
mProxyMap.put(qualifiedName, proxyInfo);
}
proxyInfo.mInjectMethods.put(value[i], executableElement);
}
}
}
} else {
continue;
}
}
}
这里提一下Element这个类的几个子类:
- TypeElement 代表类
- VariableElement 代表变量
- ExecutableElement 代表方法
首先我们调用一下mProxyMap.clear();,因为process可能会多次调用,避免生成重复的代理类,避免生成类的类名已存在异常。然后我们就是通过roundEnv.getElementsAnnotatedWith拿到我们通过@FindId和@OnClick注解的元素,然后去循环遍历这些所有注解的类、变量、方法。把他们的信息一一收集到我们的map中。(注意一定要分开注解去循环遍历处理,本文就分了两种@FindId和@OnClick,笔者写的时候就犯了此错误导致点击事件死活不生效)
收集完我们需要的所有信息后,就该去生成代理类了:
/**
* 生成代理类
*/
private void generateClass() {
for (String key : mProxyMap.keySet()) {
ProxyInfo2 proxyInfo = mProxyMap.get(key);
JavaFileObject sourceFile = null;
try {
sourceFile = mFileUtils.createSourceFile(proxyInfo.getProxyClassFullName(), proxyInfo.typeElement);
Writer writer = sourceFile.openWriter();
writer.write(proxyInfo.generateJavaCode());
writer.flush();
writer.close();
} catch (IOException e) {
error(proxyInfo.typeElement, "===tb===%s", e.getMessage());
}
}
}
这个还是很简单的,循环遍历整个map,生成我们所需要的代码。咦?生成代码?怎么生成的呢?哈哈~原来都被我们封装在ProxyInfo中了。
public String generateJavaCode() {
StringBuilder builder = new StringBuilder();
builder.append("//自动生成的注解类,勿动!!!\n");
builder.append("package ").append(packageName).append(";\n\n");
builder.append("import example.tb.com.module_api.*;\n");
builder.append("import android.support.annotation.Keep;\n");
builder.append("import android.view.View;\n");
builder.append("import " + typeElement.getQualifiedName() + ";\n");
builder.append('\n');
builder.append("@Keep").append("\n");//禁止混淆,否则反射的时候找不到该类
builder.append("public class ").append(getClassName());
builder.append(" {\n");
generateMethod(builder);
builder.append("}\n");
return builder.toString();
}
private void generateMethod(StringBuilder builder) {
builder.append(" public " + getClassName() + "(final " + typeElement.getSimpleName() + " host, View object) {\n");
if (value > 0) {
builder.append(" host.setContentView(" + value + ");\n");
}
for (int id : mInjectElements.keySet()) {
VariableElement variableElement = mInjectElements.get(id);
String name = variableElement.getSimpleName().toString();
String type = variableElement.asType().toString();
//这里object如果不为空,则可以传入view等对象
builder.append(" host." + name).append(" = ");
builder.append("(" + type + ")object.findViewById(" + id + ");\n");
}
for (int id : mInjectMethods.keySet()) {
ExecutableElement executableElement = mInjectMethods.get(id);
VariableElement variableElement = mInjectElements.get(id);
String name = variableElement.getSimpleName().toString();
builder.append(" host." + name + ".setOnClickListener(new View.OnClickListener(){\n");
builder.append(" @Override\n");
builder.append(" public void onClick(View v) {\n");
builder.append(" host." + executableElement.getSimpleName().toString() + "(host." + name + ");\n");
builder.append(" }\n");
builder.append(" });\n");
}
builder.append(" }\n");
}
看了这些生成代码的代码,你是不是瞬间觉得很low?!原来就是拼出来的代码啊。其实就是这样,看似很高深的东西,弄清楚原理之后是很简单的。如果你觉得low,那么我们再来一个高大上一些的——采用JavaPoet来替我们生成代码:
/**
* $L,原封不动
* $S,字符串
* $T,类
*
* @return
*/
public String generateJavaCode() {
ClassName viewClass = ClassName.get("android.view", "View");
ClassName keepClass = ClassName.get("android.support.annotation", "Keep");
ClassName clickClass = ClassName.get("android.view", "View.OnClickListener");
ClassName typeClass = ClassName.get(typeElement.getQualifiedName().toString().replace("." + typeElement.getSimpleName().toString(), ""), typeElement.getSimpleName().toString());
MethodSpec.Builder builder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(typeClass, "host", Modifier.FINAL)
.addParameter(viewClass, "object", Modifier.FINAL);
if (value > 0) {
builder.addStatement("host.setContentView($L)", value);
}
for (int id : mInjectElements.keySet()) {
VariableElement variableElement = mInjectElements.get(id);
String name = variableElement.getSimpleName().toString();
String type = variableElement.asType().toString();
//这里object如果不为空,则可以传入view等对象
builder.addStatement("host.$L=($L)object.findViewById($L)", name, type, id);
}
for (int id : mInjectMethods.keySet()) {
ExecutableElement executableElement = mInjectMethods.get(id);
VariableElement variableElement = mInjectElements.get(id);
String name = variableElement.getSimpleName().toString();
TypeSpec comparator = TypeSpec.anonymousClassBuilder("")
.addSuperinterface(clickClass)
.addMethod(MethodSpec.methodBuilder("onClick")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(viewClass, "view")
.addStatement("host.$L(host.$L)", executableElement.getSimpleName().toString(), name)
.returns(void.class)
.build())
.build();
builder.addStatement("host.$L.setOnClickListener($L)", name, comparator);
}
MethodSpec methodSpec = builder.build();
TypeSpec typeSpec = TypeSpec.classBuilder(getClassName())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(keepClass)
.addMethod(methodSpec)
.build();
JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();
return javaFile.toString();
}
怎么样?这样看上去是不是好多了呢,至于javapoet的使用,本文就不去专门讲解了,感兴趣的同学可以自行学习,本文最后给出了参考链接。到这里整个注解的处理过程就算讲解完了,是不是很麻烦呢,比起之前的运行时注解?
5、api的实现
最后一步就是api的实现了,也就是怎么让注解生效呢,其实这个也很简单了,我们仿照butterKinfe:
/**
* @auther tb
* @time 2018/3/27 下午2:51
* @desc 实现帮助注入的类
*/
public class TAHelper2 {
/**
* 用来缓存反射出来的类,节省每次都去反射引起的性能问题
*/
static final Map<Class<?>, Constructor<?>> BINDINGS = new LinkedHashMap<>();
public static void inject(Activity o) {
inject(o, o.getWindow().getDecorView());
}
public static void inject(Activity host, View root) {
String classFullName = host.getClass().getName() + ProxyInfo.ClassSuffix;
try {
Constructor constructor=BINDINGS.get(host.getClass());
if(constructor==null){
Class proxy = Class.forName(classFullName);
constructor=proxy.getDeclaredConstructor(host.getClass(),View.class);
BINDINGS.put(host.getClass(),constructor);
}
constructor.setAccessible(true);
constructor.newInstance(host,root);
} catch (Exception e) {
e.printStackTrace();
}
}
}
我们使用的时候只需要调用inject函数就可以实现注解注入啦~
6、注解的使用
终于写好了我们的注解了,下面就看看怎么去使用吧:
/**
* @auther tb
* @time 2018/3/23 下午2:13
* @desc
*/
@FindId(R.layout.activity_test)
public class TestActivity extends Activity {
@FindId(R.id.tv)
TextView tv;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// TAHelper.inject(this);
TAHelper2.inject(this);
if(tv!=null){
tv.setText("hahaha,how are you?");
}
}
@OnClick({R.id.tv})
void clickTest(View view){
switch (view.getId()){
case R.id.tv:
Toast.makeText(TestActivity.this,"test click",Toast.LENGTH_LONG).show();
break;
}
}
}
怎么样,是不是很简单,用起来跟butterKnife一样一样的~我们再来看看具体生成的类:
代码:
package example.tb.com.tannotation;
import android.support.annotation.Keep;
import android.view.View;
import android.view.View.OnClickListener;
import java.lang.Override;
@Keep
public class TestActivity_TA {
public TestActivity_TA(final TestActivity host, final View object) {
host.setContentView(2131296283);
host.tv=(android.widget.TextView)object.findViewById(2131165303);
host.tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
host.clickTest(host.tv);
}
});
}
}
看到这里你应该就明白了,其实就是生成一个类专门去调用activity里面的一些变量,去给他们设置一些属性,原理居然这么简单!!!所以我们的变量,方法都不能写成private,并且这个类不能被混淆,因为我们api里面是采用的反射创建的这个类,否则打包进行代码优化的时候,会去掉该类,导致注解失效,而clickTest不必像上篇中的运行时注解一样保留下来,因为这里直接调用了该方法,所以打包的时候不会被优化掉,而运行时采用的依然是反射,所以才需要keep。
7、调试
相信在编写的路上,肯定不是一次性成功那么顺利,笔者也是磕磕碰碰才算走到这一步,遇到问题最有效的方案就是调试,那么就说一下怎么去对注解处理器这块进行调试,因为它是在编译的时候执行的,所以跟日常的代码调试还不太一样。
首先我们需要配置gradle.properties。找到本地电脑的 gradle home,它一般在当前用户的目录下,比如我的 Windows 电脑位置在:C:\Users\用户名.gradle,而Mac电脑一般在:~/用户名/.gradle。打开(如果没有,新建一个)gradle.properties 文件,增加下面两行:
org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
第二步,找到菜单栏Run/Edit Configurations…打开如图窗口:
点击左上角的+,选择Remote,随便填入一个名字,采用默认配置点击ok
然后选择你刚才的名字,点击debug按钮,成功后会出现如下字符:
这样我们就可以开始调试我们的代码了。步骤如下:
选择AS菜单栏:Build/Rebuild Project,就会进入到我们在compiler的注解处理代码里面的断点了~
注意:
如果我们在点击debug按钮的时候出现上图情况,我们只需要Rebuild Project一下,然后在点击debug按钮就可以了。具体原因暂不明,有知道的同学可以告知下。
8、常见问题
疑问:这里的日志打印Messager类,笔者一直不明白怎么使用,也看不到所谓的日志,希望有知道的大神可以指点一二。
9、总结
到此我们的编译时注解就讲解完了,明白了其中原理后,写起来还是很顺手的,而需要注意的就是混淆的事项,我们在两篇文章中都讲了其中的注意事项了,如果有其他疑问的同学可以在下方留言,最后同样奉出源码:
源码下载
参考文章: