之前写过一篇是在运行期间使用注解帮助我们减少代码量的文章,如果你对注解不太熟建议先看下之前文章:http://www.jianshu.com/p/47cc73f6b966
因为在运行期间的注解使用反射性能是有降低的,所以很多框架使用的是编译期生成代码的方式比如还是Butterknife(怎么老是它,谁让它最经典用的人最多呢)在后期的版本中使用的就是编译期生成代码的方式。
我会尽量用最少的文字更多的代码来表达。最主要的是梳理逻辑(因为看网上很多说这样那样看的一头污水)。�其实逻辑想清楚了就那么回事了。不逼逼 了。先来说下流程吧。因为编译期的方式在流程上稍复杂些但也别怕,我来给你先捊捊一共就三步(不懂无所谓有个印象)咱们再开始。
github:https://github.com/EasonHolmes/AnnotationDemo
创建注解并标示@Target及@Retention
创建继承自AbstractProcessor的类用来收集注解的值及生成java源代码
-
使用反射实例化生成的源代码文件
第一步创建注解类
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
注解就不解释了这一步就这么简单
第二步创建类继承自AbstractProcessor
@AutoService(Processor.class)//生成 META-INF 信息;
@SupportedSourceVersion(SourceVersion.RELEASE_7)//声明支持的源码版本
@SupportedAnnotationTypes({"com.cui.libannotation.BindView"})//指定要处理的注解路径
//声明 Processor 处理的注解,注意这是一个数组,表示可以处理多个注解;
public class ViewInjectProcessor extends AbstractProcessor {
// 存放同一个Class下的所有注解
Map> classMap = new HashMap<>();
// 存放Class对应的TypeElement
Map classTypeElement = new HashMap<>();
private Filer filer;
Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
filer = processingEnv.getFiler();
elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean process(Set set, RoundEnvironment roundEnvironment) {
collectInfo(roundEnvironment);
writeToFile();
return true;
}
void collectInfo(RoundEnvironment roundEnvironment) {
classMap.clear();
classTypeElement.clear();
//获取所有使用到bindView的类
Set elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
for (Element element : elements) {
//获取bindvView注解的值
int viewId = element.getAnnotation(BindView.class).value();
//代表被注解的元素 variableElemet是element的子类
VariableElement variableElement = (VariableElement) element;
//被注解元素所在的class
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
//class的完整路径
String classFullName = typeElement.getQualifiedName().toString();
// 收集Class中所有被注解的元素
List variablist = classMap.get(classFullName);
if (variablist == null) {
variablist = new ArrayList<>();
VariableInfo variableInfo = new VariableInfo();
variableInfo.setVariableElement(variableElement);
variableInfo.setViewId(viewId);
variablist.add(variableInfo);
classMap.put(classFullName, variablist);
//保存class对应要素(完整路径,typeElement)
classTypeElement.put(classFullName, typeElement);
}
}
}
/**
* http://blog.csdn.net/crazy1235/article/details/51876192 javapoet使用 用来生成java文件源代码的
* 底下创建java源代码的可以看这个链接
*/
void writeToFile() {
try {
for (String classFullName : classMap.keySet()) {
TypeElement typeElement = classTypeElement.get(classFullName);
//使用构造函数绑定数据 创建一个构造函数public类型添加参数(参数类型全路径如Bundle android.os.Bundle,"参数名")
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(ParameterSpec.builder(TypeName.get(typeElement.asType()), "activity").build());
List variableList = classMap.get(classFullName);
for (VariableInfo variableInfo : variableList) {
VariableElement variableElement = variableInfo.getVariableElement();
// 变量名称(比如:TextView tv 的 tv)
String variableName = variableElement.getSimpleName().toString();
// 变量类型的完整类路径(比如:android.widget.TextView)
String variableFullName = variableElement.asType().toString();
// 在构造方法中增加赋值语句,例如:activity.tv = (android.widget.TextView)activity.findViewById(215334);
constructor.addStatement("activity.$L=($L)activity.findViewById($L)", variableName, variableFullName, variableInfo.getViewId());
}
//创建class
TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName() + "$$ViewInjector")
.addModifiers(Modifier.PUBLIC)
.addMethod(constructor.build())
.build();
//与目标class放在同一个包下,解决class属性的可访问性
String packageFullname = elementUtils.getPackageOf(typeElement).getQualifiedName().toString();
JavaFile javaFile = JavaFile.builder(packageFullname, typeSpec).build();
//生成 class文件
javaFile.writeTo(filer);
}
} catch (Exception e) {
}
}
}
其中VariableInfo就是一个简单的实体类。而@AutoService(Processor.class)
注解是用来生成 META-INF 信息 MethodSpec TypeSpec JavaFile
就是我们生成java源代码文件需要用的相关类 这两个需要依赖两个库
implementation 'com.google.auto.service:auto-service:1.0-rc3'//用于自动为 JAVA Processor 生成 META-INF 信息。
implementation 'com.squareup:javapoet:1.8.0'//封装了一套生成 .java 源文件的 API
public class VariableInfo {
int viewId;
VariableElement variableElement;
public VariableElement getVariableElement() {
return variableElement;
}
public void setVariableElement(VariableElement variableElement) {
this.variableElement = variableElement;
}
public int getViewId() {
return viewId;
}
public void setViewId(int viewId) {
this.viewId = viewId;
}
}
collectInfo方法就是用来收集使用了BindView注解的相关信息,writeToFile方法就是用来创建java文件的。类中的每一步注释都写的很清楚有不懂的可以@我。
第三步创建通过反射实例化生成的java类
public class InjectHelper {
public static void inject(Activity host) {
//获得 View 所在 Activity 的类路径,然后拼接一个字符串“$$ViewInjector”。
// 这个是编译时动态生成的 Class 的完整路径,也就是我们需要实现的,同时也是最关键的部分;
String classFullName = host.getClass().getName() + "$$ViewInjector";
try {
// 根据 Class 路径,使用 Class.forName(classFullName) 生成 Class 对象;
Class proxy = Class.forName(classFullName);
// 得到 Class 的构造函数 constructor 对象
Constructor constructor = proxy.getConstructor(host.getClass());
// 使用 constructor.newInstance(host) new 出一个对象,这会执行对象的构造方法,方法内部是我们为 MainActivity 的 tv 赋值的地方。
constructor.newInstance(host);
} catch (Exception e) {
e.printStackTrace();
}
}
}
第三步完成rebuild一下你就可以在build/generated/source/kapt/debug 下看到java源文件了
到重点了kotlin在使用上是和java有一些差别
依赖上
关键字annotationProcessor
改为kapt
这是在官方文档上也有说明
修饰上
比如我们这里的@Target(ElementType.FIELD)
在使用时要加lateinit
否则在生成的java源代码类中被注解的textview
会被认定为private
无法获取
@BindView(R.id.tv)
lateinit var textview: TextView