编译时注解
上一篇写了一个简单的运行时注解在,这边来尝试写一下编译时注解,仿照ButterKnife写一个findViewById的编译时注解
1. 新建2个java lib和一个Android lib
这里是用来放注解的lib
这里是生成我们需要的文件的地方
这里是我们连接app工程和解析注解生成的类的地方
2.去Android lib下放入一下需要的类
一个是Utils类,用来执行Android的findview
public class Utils {
public static <T extends View> T findView(Activity activity, int id){
return activity.findViewById(id);
}
}
另一个是执行bind和unbind的类,稍后再说
3. 去annotation lib下写点需要的注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface FindView {
int value();
}
这个lib基本就完成了
4. 加入配置
去app工程的build.gradle中加入上面3个lib
//注意这个要这样声明配置
annotationProcessor project(path: ':compiler')
implementation project(path: ':butterknife')
implementation project(path: ':annotation')
在整个工程的build中加入
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
在compiler的build中加入
dependencies {
implementation project(path: ':annotation')
compileOnly'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
implementation 'com.squareup:javapoet:1.9.0'
}
5.写一点Compiler内容
@AutoService(Compiler.class) //这个必须要加
public class Compiler extends AbstractProcessor {
//1. 这里是指定支持的java版本
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
//2 .这里是指定要解析的注解类型
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> set = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : getSupportedAnnotationType()){
set.add(annotation.getCanonicalName()));
}
return set;
}
public Set<Class<? extends Annotation>> getSupportedAnnotationType(){
Set<Class<? extends Annotation>> set = new LinkedHashSet<>();
set.add(FindView.class);
return set;
}
// 上面的1和2也可以和@AutoService(Compiler.class)一样写成注解形式放在类上
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
6.实现process方法
在这个方法里面我们会去仿照butterKnife去生成一些java来完成我们的findViewById这个过程
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//这里会拿到所有包含了这个注解的元素,比如说在两个activity的4个view上都写了这个注解,那么这里将会拿到这4个view
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(FindView.class);
//我们需要将view和它所在的activity对应起来
Map<Element, List<Element>> elementListMap = new HashMap<>();
for (Element element : elements) {
//返回封装此元素(非严格意义上)的最里层元素
Element enclosingElement = element.getEnclosingElement();
List<Element> list = elementListMap.get(enclosingElement);
if (list == null){
list = new ArrayList<>();
elementListMap.put(enclosingElement,list);
}
list.add(element);
}
//接下来生成对应的activityViewBind
//elementListMap里面的key是activity,value是activity里面包含注解的view。所以采用entry的方式
for (Map.Entry<Element, List<Element>> entry:elementListMap.entrySet()) {
//不管3721,先拿值
Element key = entry.getKey(); //这是activity
List<Element> value = entry.getValue(); //这是view集合
String activityName = key.getSimpleName().toString(); //获取activity的name
ClassName className = ClassName.bestGuess(activityName); //通过activity的name去猜测类名
//生成一个java class文件
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityName+"ViewBind") //创建的java类名
.addModifiers(Modifier.FINAL,Modifier.PUBLIC) //声明为 public final
.addField(className,"target",Modifier.PRIVATE); //添加一个变量 private xxxActivity target
//创建一个方法
MethodSpec.Builder methodBuild = MethodSpec.methodBuilder("unbind").addModifiers(Modifier.PUBLIC);
classBuilder.addMethod(methodBuild.build());
//创造一个结构函数
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addParameter(className,"target")//添加参数类型
.addStatement("this.target = target"); //添加一个声明?
classBuilder.addMethod(constructor.build());
//拿到包名
String packName = elementUtils.getPackageOf(key).getQualifiedName().toString();
try {
//java文件生成
JavaFile.builder(packName,classBuilder.build()).build().writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
};
}
return false;
}
Elements elementUtils;
Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
super.init(processingEnv);
}
这时候,如果配置都正确的话,可以在app工程里面给某个view加入自己的注解,点击运行
7.给每个view执行findViewById
我们继续在process中完成这个操作
for (Element element :value){
String fieldName = element.getSimpleName().toString();
//获取Utils的类名 通过包名和类名
ClassName utils = ClassName.get("com.example.butterknife", "Utils");
//获取view的id,这个在注解中有
int resId = element.getAnnotation(FindView.class).value();
//我们需要通过butterknife里面的Utils类去执行 target.view = Utils.indView(target,view)
constructor.addStatement("target.$L = $T.findView(target,$L)",fieldName,utils,resId);
}
//删掉上面的构造方法,我们放到这里去实现
classBuilder.addMethod(constructor.build()).addModifiers(Modifier.PUBLIC);
//拿到包名
String packName = elementUtils.getPackageOf(key).getQualifiedName().toString();
try {
//java文件生成
JavaFile.builder(packName,classBuilder.build()).build().writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
};
再次运行,这里把view的绑定就完成了
8. 反射获取
现在,我们要想去使用这个注解,只差最后一步了,那就是去反射获取我们创建好的类
我们去前面创建好的Android lib中实现他
public class Knife {
public static Object bind(Activity activity) {
try {
//通过传入的activity name拼接我们生成java name
Class<?> aClass = Class.forName(activity.getClass().getName() + "ViewBind");
//获取构造方法
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(activity.getClass());
//反射创建实例
Object o = declaredConstructor.newInstance(activity);
return o;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
9.使用
public class MainActivity extends AppCompatActivity {
@FindView(R.id.tv1)
TextView v1;
@FindView(R.id.tv2)
TextView v2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Knife.bind(this);
v1.setText("1111");
v2.setText("2222");
}
}
10 总结
编译时注解使用了一些java工具,这些东西在Android学习过程中很难遇到,第一次写会很懵逼,很懵逼,但是,我觉得,多写几遍多看一点总是会一点点理解的。比如说TypeSpec为我们提供了java class的创建方法,MethodSpec为我们提供了构造函数和普通方法的创建方法。MethodSpec里面的addStatement和addComment有什么区别,多写几遍,多踩点坑也就明白了。
addStatement可以给方法里面写一些具体的实现
addComment则是添加注解
具体来说就是
addStatement(“this.target = target”) 等价与
private void method(){
this.target = target
}
addComment(“this.target = target”) 等价与
private void method(){
//this.target = target
}
addReturns添加返回值
等等等等
其他的一些东西,我在代码的注解里面也写的比较清楚了,这里我们成功实现了利用编译时注解实现findViewById这样的功能,当然我们也不是自己独立写的,而是借鉴ButterKnife和其他大佬写的文章。
此外,我们还可以利用注解去实现SetOnClickListener这样的功能。当然,如果你去看过ButterKnife的源码,就会发现,没那么简单了。
11.耍流氓的方式实现点击事件注解
注解类
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
int[] onClick();
}
//解析OnClick
Set<? extends Element> clickElements = roundEnvironment.getElementsAnnotatedWith(OnClick.class);
//解析属性
Map<Element, List<Element>> clickElementListMap = new HashMap<>();
for (Element element : clickElements) {
Element enclosingElement = element.getEnclosingElement();
List<Element> list = clickElementListMap.get(enclosingElement);
if (list == null) {
list = new ArrayList<>();
clickElementListMap.put(enclosingElement, list);
}
list.add(element);
}
//生成
for (Map.Entry<Element, List<Element>> entry : clickElementListMap.entrySet()) {
Element key = entry.getKey();
List<Element> value = entry.getValue();
String activityName = key.getSimpleName().toString();
ClassName activityClassName = ClassName.bestGuess(activityName);
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityName + "ViewBinding")
.addModifiers(Modifier.FINAL, Modifier.PUBLIC)
.addField(activityClassName, "target", Modifier.PRIVATE);
MethodSpec.Builder methodbuild = MethodSpec.methodBuilder("setOnClick").addModifiers(Modifier.PUBLIC);
//classBuilder.addMethod(methodbuild.build());
//构造函数
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addParameter(activityClassName, "target");
constructor.addStatement("this.target = target");
constructor.addStatement("setOnClick()");
classBuilder.addMethod(constructor.build()).addModifiers(Modifier.PUBLIC);
//Utils.findView(source ,r.id);
for (Element element : value) {
String fieldName = element.getSimpleName().toString();
ClassName utils = ClassName.get("com.example.knife", "Utils");
OnClick annotation = element.getAnnotation(OnClick.class);
int[] resIds = annotation.onClick();
for (int i : resIds)
methodbuild.addStatement("$T.setClick(target,$L,$S)", utils, i,element.getSimpleName().toString());
}
classBuilder.addMethod(methodbuild.build()).addModifiers(Modifier.PUBLIC);
try {
String packname = elementsUtil.getPackageOf(key).getQualifiedName().toString();
JavaFile.builder(packname, classBuilder.build()).build().writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
System.out.println("error");
}
}
回到utils类中,我们在这个类里面找到对应的方法,然后利用反射去实现点击事件
public static void setClick(Object activity, int viewId, String method) {
Class<?> aClass = activity.getClass();
Method[] declaredMethods = aClass.getDeclaredMethods();
for (Method method1:declaredMethods) {
if (method1.getName().equals(method)){
View view = findView((Activity) activity, viewId);
if (view != null) {
view.setOnClickListener(new onDeclaredClick(method1, activity));
}
}
}
}
private static class onDeclaredClick implements View.OnClickListener {
private Method method;
private Object o;
public onDeclaredClick(Method method, Object o) {
this.method = method;
this.o = o;
}
@Override
public void onClick(View v) {
try {
method.setAccessible(true);
method.invoke(o);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
这里方式很耍流氓,而且这里利用了反射,反射是消耗性能的,通常不会这样写。看过butterknife源码的都知道,它只在bind的时候利用了反射,其他地方都是加载获取类等方式去实现的。
不过,重要的是,我们利用上一篇讲到的运行时注解的方式,在这里按照自己的思路实现了它。所以,事实证明,不是做不了,只是你愿不愿意去做。
另外,如果在utils里面完全按照运行时注解去写的话,会发现获取OnClick的注解时为null,这就是编译时注解和运行时注解的差距了,上一篇最后有张图(我copy来的),可以去看看