背景
在前面的文章中,讲解了注解和编译时注解等一些列相关的内容,为了更加全面和真是的了解Android
编译时注解在实战项目中的使用,本文采取实现主流框架butterknife
注入view
去全面认识编译时注解。
效果
先来张图压压惊,实现效果butterknife
的view
绑定
使用
仿照butterknife
实现了@BindView
注解,通过WzgJector.bind
方法绑定当前MainActivity
,整体和butterknife
使用完全一模一样,这里为了区分简单的把butterknife
改名了WzgJector
public class MainActivity extends AppCompatActivity { @BindView(R.id.tv_msg) TextView tvMsg; @BindView(R.id.tv_other) TextView tvOther; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); WzgJector.bind(this); if(tvMsg!=null){ tvMsg.setText("我已经成功初始化了"); } if(tvOther!=null){ tvOther.setText("我就来看看而已"); } }}复制代码
实现
实现的思路和实现原理大致一样,所以这里不重复阐述重复的步骤,重点讲解提升的技术点,所以需要在了解基本编译时注解的前提下继续下面的学习
定义注解
这里使用了java
和android
自带的注解,初始一个BindView
注解,同时指定了@Target
为FIELD
,注解BindView
带有一个初始的int
参数及时使用时的view-id
@Retention(RetentionPolicy.CLASS)@Target(ElementType.FIELD)public @interface BindView { int value();}复制代码
对java
和android
自带的注解不太清楚的同学可参考下面两篇文章
Element
详解
Element
有了注解,必然需要有一个对应的注解处理器去处理注解,但是在处理注解的时候需要充分的了解注解处理器中的process
方法及时核心的编译代码,而process
方法的核心便是Element
对象,所以在讲解注解处理器前,需要对Element
有全面的认识,方能事半功倍。
由于Element
的知识内容的复杂性,这里重点讲解核心内容,基本使用完全是足够了
源码:
public interface Element extends AnnotatedConstruct { TypeMirror asType(); ElementKind getKind(); SetgetModifiers(); Name getSimpleName(); Element getEnclosingElement(); List getEnclosedElements(); boolean equals(Object var1); int hashCode(); List getAnnotationMirrors(); A getAnnotation(Class var1); R accept(ElementVisitor var1, P var2);}复制代码
可看出其实Element
是定义的一个接口,定义了外部调用暴露出的接口
方法 | 解释 |
---|---|
asType | 返回此元素定义的类型 |
getKind | 返回此元素的种类:包、类、接口、方法、字段...,如下枚举值 |
getModifiers | 返回此元素的修饰符,如下枚举值 |
getSimpleName | 返回此元素的简单名称,比如activity名 |
getEnclosingElement | 返回封装此元素的最里层元素,如果此元素的声明在词法上直接封装在另一个元素的声明中,则返回那个封装元素; 如果此元素是顶层类型,则返回它的包如果此元素是一个包,则返回 null; 如果此元素是一个泛型参数,则返回 null. |
getAnnotation | 返回此元素针对指定类型的注解(如果存在这样的注解),否则返回 null。注解可以是继承的,也可以是直接存在于此元素上的 |
getKind
方法
其中getKind
方法比较特殊,getKind()
方法来获取具体的类型,方法返回一个枚举值TypeKind
源码:
public enum TypeKind { /** The primitive type { @code boolean}. */ BOOLEAN, /** The primitive type { @code byte}. */ BYTE, /** The primitive type { @code short}. */ SHORT, /** The primitive type { @code int}. */ INT, /** The primitive type { @code long}. */ LONG, /** The primitive type { @code char}. */ CHAR, /** The primitive type { @code float}. */ FLOAT, /** The primitive type { @code double}. */ DOUBLE, /** The pseudo-type corresponding to the keyword { @code void}. */ VOID, /** A pseudo-type used where no actual type is appropriate. */ NONE, /** The null type. */ NULL, /** An array type. */ ARRAY, /** A class or interface type. */ DECLARED, /** A class or interface type that could not be resolved. */ ERROR, /** A type variable. */ TYPEVAR, /** A wildcard type argument. */ WILDCARD, /** A pseudo-type corresponding to a package element. */ PACKAGE, /** A method, constructor, or initializer. */ EXECUTABLE, /** An implementation-reserved type. This is not the type you are looking for. */ OTHER, /** A union type. */ UNION, /** An intersection type. */ INTERSECTION; }复制代码
Element
子类
Element
有五个直接子接口,它们分别代表一种特定类型的元素
Tables | Are |
---|---|
TypeElement | 一个类或接口程序元素 |
VariableElement | 一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数 |
ExecutableElement | 某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素 |
PackageElement | 一个包程序元素 |
TypeParameterElement | 一般类、接口、方法或构造方法元素的泛型参数 |
五个子类各有各的用处并且有各种独立的方法,在使用的时候可以强制将Element
对象转换成其中的任一一种,但是前提是满足条件的转换,不然会抛出异常。
其中最核心的两个子分别是TypeElement
和VariableElement
TypeElement
详解
TypeElement
定义的一个类或接口程序元素,相当于当前注解所在的class
对象,及时本案例使用代码中的MainActivity
源码如下:
public interface TypeElement extends Element, Parameterizable, QualifiedNameable { List getEnclosedElements(); NestingKind getNestingKind(); Name getQualifiedName(); Name getSimpleName(); TypeMirror getSuperclass(); List getInterfaces(); List getTypeParameters(); Element getEnclosingElement();}复制代码
这里讲解主要的方法的含义
方法 | 解释 |
---|---|
getNestingKind | 返回此类型元素的嵌套种类 |
getQualifiedName | 返回此类型元素的完全限定名称。更准确地说,返回规范 名称。对于没有规范名称的局部类和匿名类,返回一个空名称. |
getSuperclass | 返回此类型元素的直接超类。如果此类型元素表示一个接口或者类 java.lang.Object,则返回一个种类为 NONE 的 NoType |
getInterfaces | 返回直接由此类实现或直接由此接口扩展的接口类型 |
getTypeParameters | 按照声明顺序返回此类型元素的形式类型参数 |
VariableElement
详解
源码:
public interface VariableElement extends Element { Object getConstantValue(); Name getSimpleName(); Element getEnclosingElement();}复制代码
这里VariableElement
除了拥有Element
的方法以外还有以下两个方法
方法 | 解释 |
---|---|
getConstantValue | 变量初始化的值 |
getEnclosingElement | 获取相关类信息 |
注解处理器
注解处理器需要两个步骤的处理:
1.收集先关的信息
2.生成处理类
对Element
有了全面的了解过后,注解处理器便可很轻松的学习了,先来看看简单版本的BindView
处理
Set elements = roundEnv.getElementsAnnotatedWith(BindView.class); //一、收集信息 for (Element element : elements) { /*检查类型*/ if (!(element instanceof VariableElement)) { return false; } VariableElement variableElement = (VariableElement) element; /*获取类信息*/ TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement(); /*类的绝对路径*/ String qualifiedName = typeElement.getQualifiedName().toString(); /*类名*/ String clsName = typeElement.getSimpleName().toString(); /*获取包名*/ String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString(); BindView annotation = variableElement.getAnnotation(BindView.class); int id = annotation.value(); /*参数名*/ String name = variableElement.getSimpleName().toString(); /*参数对象类*/ String type = variableElement.asType().toString(); ClassName InterfaceName = ClassName.bestGuess("com.example.annotation.api.ViewInjector"); ClassName host = ClassName.bestGuess(qualifiedName); MethodSpec main = MethodSpec.methodBuilder("inject") .addModifiers(Modifier.PUBLIC) .returns(void.class) .addAnnotation(Override.class) .addParameter(host, "host") .addParameter(Object.class, "object") .addCode("" + " if(object instanceof android.app.Activity){\n" + " host." + name + " = (" + type + ")(((android.app.Activity)object).findViewById(" + id + "));\n" + " }\n" + "else{\n" + " host." + name + " = (" + type + ")(((android.view.View)object).findViewById(" + id + "));\n" + "}\n") .build(); TypeSpec helloWorld = TypeSpec.classBuilder(clsName + "ViewInjector") .addModifiers(Modifier.PUBLIC) .addMethod(main) .addSuperinterface(ParameterizedTypeName.get(InterfaceName, host)) .build(); try { // 生成 com.example.HelloWorld.java JavaFile javaFile = JavaFile.builder(packageName, helloWorld) .addFileComment(" This codes are generated automatically. Do not modify!") .build(); // 生成文件 javaFile.writeTo(filer); } catch (IOException e) { e.printStackTrace(); }复制代码
大体的思路,先判断Element
类型,如果是VariableElement
则继续获取相关的包名(这里必须在app
包名一致,不然获取不到android
类)类对象信息,以及@BindView
注解修饰的参数数据;最后将所有需要的数据通过javapoet
和Filer
自动编译创建一个java文件
最后得到的生成类:
package com.wzgiceman.viewinjector;import com.example.ViewInjector;import java.lang.Object;import java.lang.Override;public class MainActivityViewInjector implements ViewInjector{ @Override public void inject(MainActivity host, Object object) { if(object instanceof android.app.Activity){ host.tvMsg = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492945)); } else{ host.tvMsg = (android.widget.TextView)(((android.view.View)object).findViewById(2131492945)); } }}复制代码
上面的简单处理器中,只是单纯的判断一个注解情况,在信息收集的处理上简化了,导致当前处理器只能同时处理当前相同类中的莫一个注解这里只初始化了tvMsg
对象,tvOther
并没有初始化,当然这是不符合实际需求的,下面来优化收集和处理方案。
优化
优化方案其实就是多了一步信息的记录的工作
创建信息类对象
首先创建一个类信息对象,其中包含了一下的属性,其中varMap
便是记录当前类中所有注解相关的信息
public class VariMsg { /*包名*/ private String pk; /*类名*/ private String clsName; /*注解对象*/ private HashMapvarMap; }复制代码
BindViewProcessors
1.初始一个map记录VariMsg
对象,因为process方法可能会多次调用,所以需要每次都clear
一遍
MapveMap = new HashMap<>();复制代码
2.记录信息
通过veMap
记录所有的相关信息,并且每次需要判断是否重复,剔除重复的数据。 for (Element element : elements) { /*检查类型*/ if (!(element instanceof VariableElement)) { return false; } VariableElement variableElement = (VariableElement) element; /*获取类信息*/ TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement(); /*类的绝对路径*/ String qualifiedName = typeElement.getQualifiedName().toString(); /*类名*/ String clsName = typeElement.getSimpleName().toString(); /*获取包名*/ String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString(); BindView annotation = variableElement.getAnnotation(BindView.class); int id = annotation.value(); VariMsg variMsg = veMap.get(qualifiedName); if (variMsg == null) { variMsg = new VariMsg(packageName, clsName); variMsg.getVarMap().put(id, variableElement); veMap.put(qualifiedName, variMsg); } else { variMsg.getVarMap().put(id, variableElement); } }复制代码
3.通过javapoet
去生成java
类文件
javapoet
的运用,详细用法可去javapoet
查看 System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); for (String key : veMap.keySet()) { ClassName InterfaceName = ClassName.bestGuess("com.example.ViewInjector"); ClassName host = ClassName.bestGuess(key); VariMsg variMsg = veMap.get(key); StringBuilder builder = new StringBuilder(); builder.append(" if(object instanceof android.app.Activity){\n"); builder.append(code(variMsg.getVarMap(), "android.app.Activity")); builder.append("}\n"); builder.append("else{\n"); builder.append(code(variMsg.getVarMap(), "android.view.View")); builder.append("}\n"); MethodSpec main = MethodSpec.methodBuilder("inject") .addModifiers(Modifier.PUBLIC) .returns(void.class) .addAnnotation(Override.class) .addParameter(host, "host") .addParameter(Object.class, "object") .addCode(builder.toString()) .build(); TypeSpec helloWorld = TypeSpec.classBuilder(variMsg.getClsName() + "ViewInjector") .addModifiers(Modifier.PUBLIC) .addMethod(main) .addSuperinterface(ParameterizedTypeName.get(InterfaceName, host)) .build(); try { JavaFile javaFile = JavaFile.builder(variMsg.getPk(), helloWorld) .addFileComment(" This codes are generated automatically. Do not modify!") .build(); javaFile.writeTo(filer); } catch (IOException e) { e.printStackTrace(); System.out.println("e--->" + e.getMessage()); } } System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); return true; } /** * 根据注解对象生成code方法体 * * @param map * @param pk * @return */ private String code(Mapmap, String pk) { StringBuilder builder = new StringBuilder(); for (Integer id : map.keySet()) { VariableElement variableElement = map.get(id); String name = variableElement.getSimpleName().toString(); String type = variableElement.asType().toString(); builder.append("host." + name + " = (" + type + ")(((" + pk + ")object).findViewById(" + id + "));\n"); } return builder.toString(); }复制代码
到这里注解处理器最终版本就生成成功了,看下最后生成的代码类
package com.wzgiceman.viewinjector;import com.example.ViewInjector;import java.lang.Object;import java.lang.Override;public class MainActivityViewInjector implements ViewInjector{ @Override public void inject(MainActivity host, Object object) { if(object instanceof android.app.Activity){ host.tvMsg = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492945)); host.tvOther = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492946)); } else{ host.tvMsg = (android.widget.TextView)(((android.view.View)object).findViewById(2131492945)); host.tvOther = (android.widget.TextView)(((android.view.View)object).findViewById(2131492946)); } }}复制代码
api
api
模块主要定义的是给外部提供的使用方法,这里使用方法便是WzgJector.bind(this)
方法,相同于Butter Knife
中的ButterKnife.bind(this);
public class MainActivity extends AppCompatActivity { xxxxxx @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); WzgJector.bind(this); xxxx }}复制代码
实现
创建一个app module
定义接口类ViewInjector
暴露的外部方法,主要在编译自动生成的注解处理类中使用
/** * 接口 * Created by WZG on 2017/1/11. */public interface ViewInjector{ void inject(M m, Object object);}复制代码
实际处理类WzgJector
提供了两个方法,一种是activity
绑定,一种是view
或者fragment
绑定,绑定完以后,通过反射得到相关注解编译处理类及时ViewInjector
子类对象,调用inject(M m, Object object)
方法完成初始过程。
public class WzgJector { public static void bind(Object activity) { bind(activity, activity); } public static void bind(Object host, Object root) { Class clazz = host.getClass(); String proxyClassFullName = clazz.getName() + "ViewInjector"; try { Class proxyClazz = Class.forName(proxyClassFullName); ViewInjector viewInjector = (ViewInjector) proxyClazz.newInstance(); viewInjector.inject(host, root); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } }}复制代码
到这里其实会发现,编译时注解并不是完全不使用反射,但是它避免了关键性重复性代码的多次反射使用,继而提升了编译的性能。
结果
到这里butterknife
的@BindView
实现原理基本就是如此,由于时间原因@OnClick
就不再讲解了。其实原理一样,小伙伴们可以自己安装本文思路添加@OnClick
的处理。