前言
关于注解的基础知识,可以参考另一篇随笔——注解 ,这里不再复述。
注解的保留时间分为三种:
- SOURCE——只在源代码中保留,编译器将代码编译成字节码文件后就会丢掉
- CLASS——保留到字节码文件中,但Java虚拟机将class文件加载到内存是不一定在内存中保留
- RUNTIME——一直保留到运行时
通常我们使用后两种,因为SOURCE主要起到标记方便理解的作用,无法对代码逻辑提供有效的信息。
时间 | 解析 | 性能影响 | |
---|---|---|---|
RUNTIME | 运行时 | 反射 | 有 |
CLASS | 编译期 | APT+JavaPoet | 无 |
如上图,对比两种解析方式:
- 运行时注解比较简单易懂,可以运用反射技术在程序运行时获取指定的注解信息,因为用到反射,所以性能会收到一定影响。
- 编译期注解可以使用APT(Annotation Processing Tool)技术,在编译期扫描和解析注解,并结合JavaPoet技术生成新的java文件,是一种更优雅的解析注解的方式,不会对程序性能产生太大影响。
下面以BindView为例,介绍两种方式的不同使用方法。
运行时注解
运行时注解主要通过反射进行解析,代码运行过程中,通过反射我们可以知道哪些属性、方法使用了该注解,并且可以获取注解中的参数,做一些我们想做的事情。
首先,新建一个注解
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface BindViewTo { int value() default -1; //需要绑定的view id }
然后,新建一个注解解析工具类AnnotationTools,和一般的反射用法并无不同:
public class AnnotationTools { public static void bindAllAnnotationView(Activity activity) { //获得成员变量 Field[] fields = activity.getClass().getDeclaredFields(); for (Field field : fields) { try { if (field.getAnnotations() != null) { //判断BindViewTo注解是否存在 if (field.isAnnotationPresent(BindViewTo.class)) { //获取访问权限 field.setAccessible(true); BindViewTo getViewTo = field.getAnnotation(BindViewTo.class); //获取View id int id = getViewTo.value(); //通过id获取View,并赋值该成员变量 field.set(activity, activity.findViewById(id)); } } } catch (Exception e) { } } } }
在Activity中调用
public class MainActivity extends AppCompatActivity { @BindViewTo(R.id.text) private TextView mText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //调用注解绑定,当前Activity中所有使用@BindViewTo注解的控件将自动绑定 AnnotationTools.bindAllAnnotationView(this); //测试绑定是否成功 mText.setTextColor(Color.RED); } }
测试结果毫无意外,字体变成了红色,说明绑定成功。
编译期注解(APT+JavaPoet)
编译期注解解析需要用到APT(Annotation Processing Tool)技术,APT是javac中提供的一种编译时扫描和处理注解的工具,它会对源代码文件进行检查,并找出其中的注解,然后根据用户自定义的注解处理方法进行额外的处理。APT工具不仅能解析注解,还能结合JavaPoet技术根据注解生成新的的Java源文件,最终将生成的新文件与原来的Java文件共同编译。
APT实现流程如下:
- 创建一个java lib作为注解解析库——如apt_processor
- 在创建一个java lib作为注解声明库——如apt_annotation
- 搭建两个lib和主项目的依赖关系
- 实现AbstractProcessor
- 编译和调用
整个流程是固定的,我们的主要工作是继承AbstractProcessor,并且实现其中四个方法。下面一步一步详细介绍:
(1)创建解析库apt_processor
apply plugin: 'java-library' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) compile 'com.squareup:javapoet:1.9.0' // square开源的 Java 代码生成框架 compile 'com.google.auto.service:auto-service:1.0-rc2' //Google开源的用于注册自定义注解处理器的工具 implementation project(':apt_annotation') //依赖自定义注解声明库 } sourceCompatibility = "7" targetCompatibility = "7"
(2)创建注解库apt_annotation
声明一个注解BindViewTo,注意@Retention不再是RUNTIME,而是CLASS。
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.CLASS) public @interface BindViewTo { int value() default -1; }
(3)搭建主项目依赖关系
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':apt_annotation') //依赖自定义注解声明库 annotationProcessor project(':apt_processor') //依赖自定义注解解析库(仅编译期) }
这里需要解释一下,因为注解解析库只在程序编译期有用,没必要打包进APK。所以依赖解析库使用的关键字是annotationProcessor,这是google为gradle插件添加的特性,表示只在编译期依赖,不会打包进最终APK。这也是为什么前面要把注解声明和注解解析拆分成两个库的原因。因为注解声明是一定要编译到最终APK的,而注解解析不需要。
(4)实现AbstractProcessor
这是最复杂的一步,也是完成我们期望工作的重点。首先,我们在apt_processor中创建一个继承自AbstractProcessor的子类,重载其中四个方法:
- init()——此处初始化一个工具类
- getSupportedSourceVersion()——声明支持的Java版本,一般为最新版本
- getSupportedAnnotationTypes()——声明支持的注解列表
- process()——编译器回调方法,apt核心实现方法
具体代码如下:
//@SupportedSourceVersion(SourceVersion.RELEASE_7) //@SupportedAnnotationTypes("com.xibeixue.apt_annotation.BindViewTo") @AutoService(Processor.class) public class BindViewProcessor extends AbstractProcessor { private Elements mElementUtils; private HashMap<String, BinderClassCreator> mCreatorMap = new HashMap<>(); /** * init方法一般用于初始化一些用到的工具类,主要有 * processingEnvironment.getElementUtils(); 处理Element的工具类,用于获取程序的元素,例如包、类、方法。 * processingEnvironment.getTypeUtils(); 处理TypeMirror的工具类,用于取类信息 * processingEnvironment.getFiler(); 文件工具 * processingEnvironment.getMessager(); 错误处理工具 */ @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); mElementUtils = processingEnv.getElementUtils(); } /** * 获取Java版本,一般用最新版本 * 也可以使用注解方式:@SupportedSourceVersion(SourceVersion.RELEASE_7) */ @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } /** * 获取目标注解列表 * 也可以使用注解方式:@SupportedAnnotationTypes("com.xibeixue.apt_annotation.BindViewTo") */ @Override public Set<String> getSupportedAnnotationTypes() { HashSet<String> supportTypes = new LinkedHashSet<>(); supportTypes.add(BindViewTo.class.getCanonicalName()); return supportTypes; } /** * 编译期回调方法,apt核心实现方法 * 包含所有使用目标注解的元素(Element) */ @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { //扫描整个工程, 找出所有使用BindViewTo注解的元素 Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindViewTo.class); //遍历元素, 为每一个类元素创建一个Creator for (Element element : elements) { //BindViewTo限定了只能属性使用, 这里强转为变量元素VariableElement VariableElement variableElement = (VariableElement) element; //获取封装属性元素的类元素TypeElement TypeElement classElement = (TypeElement) variableElement.getEnclosingElement(); //获取简单类名 String fullClassName = classElement.getQualifiedName().toString(); BinderClassCreator creator = mCreatorMap.get(fullClassName); //如果不存在, 则创建一个对应的Creator if (creator == null) { creator = new BinderClassCreator(mElementUtils.getPackageOf(classElement), classElement); mCreatorMap.put(fullClassName, creator); } //将需要绑定的变量和对应的view id存储到对应的Creator中 BindViewTo bindAnnotation = variableElement.getAnnotation(BindViewTo.class); int id = bindAnnotation.value(); creator.putElement(id, variableElement); } //每一个类将生成一个新的java文件,其中包含绑定代码 for (String key : mCreatorMap.keySet()) { BinderClassCreator binderClassCreator = mCreatorMap.get(key); //通过javapoet构建生成Java类文件 JavaFile javaFile = JavaFile.builder(binderClassCreator.getPackageName(), binderClassCreator.generateJavaCode()).build(); try { javaFile.writeTo(processingEnv.getFiler()); } catch (IOException e) { e.printStackTrace(); } } return false; } }
其中,BinderClassCreator是代码生成相关方法,具体代码如下:
public class BinderClassCreator { public static final String ParamName = "rootView"; private TypeElement mTypeElement; private String mPackageName; private String mBinderClassName; private Map<Integer, VariableElement> mVariableElements = new HashMap<>(); /** * @param packageElement 包元素 * @param classElement 类元素 */ public BinderClassCreator(PackageElement packageElement, TypeElement classElement) { this.mTypeElement = classElement; mPackageName = packageElement.getQualifiedName().toString(); mBinderClassName = classElement.getSimpleName().toString() + "_ViewBinding"; } public void putElement(int id, VariableElement variableElement) { mVariableElements.put(id, variableElement); } public TypeSpec generateJavaCode() { return TypeSpec.classBuilder(mBinderClassName) //public 修饰类 .addModifiers(Modifier.PUBLIC) //添加类的方法 .addMethod(generateMethod()) //构建Java类 .build(); } private MethodSpec generateMethod() { //获取全类名 ClassName className = ClassName.bestGuess(mTypeElement.getQualifiedName().toString()); //构建方法--方法名 return MethodSpec.methodBuilder("bindView") //public方法 .addModifiers(Modifier.PUBLIC) //返回void .returns(void.class) //方法传参(参数全类名,参数名) .addParameter(className, ParamName) //方法代码 .addCode(generateMethodCode()) .build(); } private String generateMethodCode() { StringBuilder code = new StringBuilder(); for (int id : mVariableElements.keySet()) { VariableElement variableElement = mVariableElements.get(id); //变量名称 String name = variableElement.getSimpleName().toString(); //变量类型 String type = variableElement.asType().toString(); //rootView.name = (type)view.findViewById(id), 注意原类中变量声明不能为private,否则这里是获取不到的 String findViewCode = ParamName + "." + name + "=(" + type + ")" + ParamName + ".findViewById(" + id + "); "; code.append(findViewCode); } return code.toString(); } public String getPackageName() { return mPackageName; } }
(5)编译和调用
在MainActivity中调用,这里需要强调的是待绑定变量不能声明为private,原因在上面代码注释中已经解释了。
public class MainActivity extends AppCompatActivity { @BindViewTo(R.id.text) public TextView mText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);//这里的MainActivity需要先编译生成后才能调用 new MainActivity_ViewBinding().bindView(this); //测试绑定是否成功 mText.setTextColor(Color.RED); } }
此时,build或rebuild工程(需要先注掉MainActivity的调用),会看到在generatedJava文件夹下生成了新的Java文件。
上面的调用方式需要先编译一次才能使用,当有多个Activity时比较繁琐,而且无法做到统一。
我们也可以选择另一种更简便的方法,即反射调用。新建工具类如下:
public class MyButterKnife { public static void bind(Activity activity) { Class clazz = activity.getClass(); try { Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding"); Method method = bindViewClass.getMethod("bindView", activity.getClass()); method.invoke(bindViewClass.newInstance(), activity); } catch (Exception e) { e.printStackTrace(); } } }
调用方式改为:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //通过反射调用 MyButterKnife.bind(this); //测试绑定是否成功 mText.setTextColor(Color.RED); }
此方式虽然也会稍微影响性能,但依然比直接使用运行时注解高效得多。
总结
说到底,APT是一个编译器工具,是一个非常好的从源码到编译期的过渡解析工具。虽然结合JavaPoet技术被各大框架使用,但是依然存在固有的缺陷,比如变量不能私有,依然要采用反射调用等,普通开发者可斟酌使用。
个人认为APT有如下优点:
- 配置方式,替换文件配置方式,改为代码内配置,提高程序内聚性
- 代码精简,一劳永逸,省去繁琐复杂的格式化代码,适合团队内推广
以上优点同时也是缺点,因为很多代码都在后台生成,会对新同学造成理解困难,影响其对整体架构的理解,增加学习成本。
近期研究热修复和APT,发现从我们写完成代码,到代码真正执行,期间还真是有大把的“空子”可以钻啊,借图mark一下。