编译时注解之butterknife的简单实现
1. 前提
本博客就是为了让我自己更好的理解butterknife的原理,或者是更好的让大家学习一下运行时注解,所以本博客的大前提是在参考 张鸿洋 的 Android如何编写基于编译时注解的项目 编写的,但是 张鸿洋 大神有很多地方没有解释到,本篇文章可以让初学者,学习到怎样使用运行时注解.可以为大家更好的除去疑惑
2. 开始编写
2.1 框架分为四个模块,前三个为核心模块:
- butterknife-annotations: 自定义注解模块,Java Library类型模块
- butterknife-compiler: 注解处理器模块,用于处理注解并生成文件,Java Library类型模块
- butterknife-api: 框架api模块,供使用者调用,Android Library类型模块
butterknife-demo: 示例Demo模块,Android工程类型模块
依赖关系:butterknife-compiler 依赖 butterknife-annotationbutterknife-api 依赖 butterknife-annotation butterknife-demo 依赖 butterknife-api 和 butterknife-compiler
2.2 为什么要分为3大模块?
因为注解处理模块器butterknife-compiler只在我们编译过程中需要使用到,在APP运行阶段就不需要使用该模块了。所以在发布APP时,我们就不必把注解处理器模块打包进来,以免造成程序臃肿,所以把butterknife-compiler模块单独拿出来。同时注解处理模块和api模块都需要使用到自定义注解模块,所以就需要把自定义注解模块单独拿出来。这样为何需要分成三个模块的原因也就一目了然了,其实butterfnife框架也是这样分的。
3 编写注解模块(butterknife-annotations)
我们只编写一个bindview注解,其他的注解一样的
//编译型注解@Retention(RetentionPolicy.CLASS)//注解在成员变量上@Target(ElementType.FIELD)public @interface BindView { int value();}
如果还有不懂 Retention 和 Target 的同学,请看我上篇文章 注解的详细介绍
4 注解处理器模块(butterknife-compiler)
4.1 注册你写的处理器
要像jvm调用你写的处理器,你必须先注册,让他知道。怎么让它知道呢,其实很简单,google 为我们提供了一个库,简单的一个注解就可以。
在此build.gradle中添加依赖
compile ('com.google.auto.service:auto-service:1.0-rc2')
auto-service库可以帮我们去生成META-INF等信息。可以自动生成META-INF/services/javax.annotation.processing.Processor文件(该文件是所有注解处理器都必须定义的),免去了我们手动配置的麻烦。
4.2 编写处理器
所有的注解处理器都必须继承AbstractProcessor,所以我们也编写一个ButterknifeAbstractProcessor类,一般重写四个方法
@AutoService(Processor.class)public class ButterknifeAbstractProcessor extends AbstractProcessor { //存储信息 private Map mProxyMap = new HashMap(); private Filer mFileUtils; private Elements mElementUtils; /** * Element 的子类 - VariableElement //一般代表成员变量 - ExecutableElement //一般代表类中的方法 - TypeElement //一般代表代表类 - PackageElement //一般代表Package */ private Messager mMessager;/** * //初始化父类的方法 * @param processingEnv */@Overridepublic synchronized void init(ProcessingEnvironment processingEnv){ super.init(processingEnv); //跟文件相关的辅助类,生成JavaSourceCode. mFileUtils = processingEnv.getFiler(); //跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。 mElementUtils = processingEnv.getElementUtils(); //跟日志相关的辅助类 mMessager = processingEnv.getMessager();}/** * 返回支持的注解类型 一般都是固定写法一般都是固定写法 * @return */@Overridepublic Set getSupportedAnnotationTypes(){ /** Class * getName my.ExternalClassConfig getCanonicalName my.ExternalClassConfig getSimpleName ExternalClassConfig getName my.ExternalClassConfig$InternalConfig getCanonicalName my.ExternalClassConfig.InternalConfig getName()返回的是虚拟机里面的class的表示,而getCanonicalName()返回的是更容易理解的表示。其实对于大部分class来说这两个方法没有什么不同的。但是对于array或内部类来说是有区别的。 另外,类加载(虚拟机加载)的时候需要类的名字是getName */ Set annotationTypes = new LinkedHashSet(); annotationTypes.add(BindView.class.getCanonicalName()); return annotationTypes;}/** * 返回支持的源码版本 一般都写最近的 一般都是固定写法 * @return */@Overridepublic SourceVersion getSupportedSourceVersion(){ return SourceVersion.latestSupported();}/**作用: * 1.收集信息 * 就是根据你的注解声明,拿到对应的Element,然后获取到我们所需要的信息,这个信息肯定是为了后面生成JavaFileObject所准备的 * 2 .生成代理类(本文把编译时生成的类叫代理类) * 我们会针对每一个类生成一个代理类,例如MainActivity我们会生成一个MainActivity$$ViewInjector * * * @param set * @param roundEnvironment * @return */@Overridepublic boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { //因为process可能会多次调用,避免生成重复的代理类,避免生成类的类名已存在异常 mProxyMap.clear(); // 通过roundEnvironment.getElementsAnnotatedWith拿到我们通过@BindView注解的元素,这里返回值,按照我们的预期应该是VariableElement集合,因为我们用于成员变量上。 Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class); //一、收集信息 for (Element element : elements){ //field type 拿到成员变量 VariableElement variableElement = (VariableElement) element; //class type 类 拿到对应的类信息 从而生成生成ProxyInfo对象 TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();//TypeElement String qualifiedName = typeElement.getQualifiedName().toString(); ProxyInfoClass proxyInfoClass = mProxyMap.get(qualifiedName); if (proxyInfoClass == null){ proxyInfoClass = new ProxyInfoClass(mElementUtils, typeElement) ; mProxyMap.put(qualifiedName, proxyInfoClass); } BindView annotation = variableElement.getAnnotation(BindView.class); int id = annotation.value(); proxyInfoClass.injectVariables.put(id, variableElement); } // 生成代理类 用i/o流 写成一个代理类 for(String key : mProxyMap.keySet()){ ProxyInfoClass proxyInfoClass = mProxyMap.get(key); JavaFileObject sourceFile = null; try { sourceFile = mFileUtils.createSourceFile( proxyInfoClass.getProxyClassFullName(), proxyInfoClass.getTypeElement()); Writer writer = sourceFile.openWriter(); writer.write(proxyInfoClass.generateJavaCode()); writer.flush(); writer.close(); } catch (IOException e) { e.printStackTrace(); } } return true;}
- init(ProcessingEnvironment processingEnvironment): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer。
- process(Set
4.3 简单介绍几个名词
Element:代表程序中的元素,比如说 包,类,方法。每一个元素代表一个静态的,语言级别的结构.
比如:public class Perison { // TypeElementprivate int name; // VariableElementpublic void setName(String name) // ExecuteableElement}
Filer mFileUtils; 跟文件相关的辅助类,生成JavaSourceCode.
- Elements mElementUtils;跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
- Messager mMessager;跟日志相关的辅助类。
4.4 生成代理类
// 开始编写代理类proxyInfoClass.generateJavaCode()
手写生成代理类,同样我们也可以使用 javapoet 其实就是把字符串简化了一下
public class ProxyInfoClass{ private String packageName; private String proxyClassName; private TypeElement typeElement;public Map injectVariables = new HashMap<>();// 这个是 api 库中的方法名public static final String PROXY = "ViewInject";public ProxyInfoClass(Elements elementUtils, TypeElement classElement){ this.typeElement = classElement; PackageElement packageElement = elementUtils.getPackageOf(classElement); String packageName = packageElement.getQualifiedName().toString(); //classname String className = ClassValidator.getClassName(classElement, packageName); this.packageName = packageName; this.proxyClassName = className + "_" + PROXY;}public String generateJavaCode(){ StringBuilder builder = new StringBuilder(); builder.append("// Generated code. Do not modify!\n"); // builder.append("package ").append(packageName).append(";\n\n"); //包名改一下 builder.append("import cn.nzy.butterknife_api.*;\n"); builder.append('\n'); builder.append("public class ").append(proxyClassName).append(" implements " + ProxyInfoClass.PROXY + "<" + typeElement.getQualifiedName() + ">"); builder.append(" {\n"); generateMethods(builder); builder.append('\n'); builder.append("}\n"); return builder.toString();}private void generateMethods(StringBuilder builder){ builder.append("@Override\n "); // 方法名 更改一下 builder.append("public void inject(" + typeElement.getQualifiedName() + " host, Object source ) {\n"); for (int id : injectVariables.keySet()) { VariableElement element = injectVariables.get(id); String name = element.getSimpleName().toString(); String type = element.asType().toString(); builder.append(" if(source instanceof android.app.Activity){\n"); builder.append("host." + name).append(" = "); builder.append("(" + type + ")(((android.app.Activity)source).findViewById( " + id + "));\n"); builder.append("\n}else{\n"); builder.append("host." + name).append(" = "); builder.append("(" + type + ")(((android.view.View)source).findViewById( " + id + "));\n"); builder.append("\n};"); } builder.append(" }\n");}public String getProxyClassFullName(){ return packageName + "." + proxyClassName;}public TypeElement getTypeElement(){ return typeElement;}
}
4.5 build生成真正生成代理类
点击Build->rebuild project 就会生成 对应activity的代理类
路径在 : ButterKnife\butterknife_demo\build\generated\source\apt\debug\cn\nzy\butterknife_demo\MainActivity_ViewInject.java
如果build的时候遇到错误,下面总结一波错误:
编码GBK的不可映射字符的错误
因为 如果AbstractProcessor中或者ProxyInfoClass有中文, 会影响i/o流写入文件,所以在每一个build.gralde中加入tasks.withType(JavaCompile) { options.encoding = "UTF-8" }
javac错误
Error:Execution failed for task ':butterknife-demo:javaPreCompileDebug'. > Annotation processors must be explicitly declared now. The following dependencies on the compile classpath are found to contain annotation processor. Please add them to the annotationProcessor configuration. - butterknife-compiler.jar (project :butterknife-compiler) Alternatively
解决方法是:在demo的build.gradle中
defaultConfig { //... 你的东西 javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } } }
如果MainActivity_ViewInject.java这个代理类中有错误
请核对好 ProxyInfoClass.java 中的方法 和 注释
4.6 检验
在demo的Activity中写入demo,检验即可
demo地址 github
参考文章
Android 如何编写基于编译时注解的项目 (也可以说转载人家的)
利用APT实现Android编译时注解
深入理解ButterKnife源码并掌握原理(一)
更多相关文章
- Android新技术------Android(安卓)App Bundle之bundletool的使用
- Android中使用javah生成jni头文件的正确方法
- Android高级工程师BAT面试题及知识点整理大全(Java基础+Android模
- 产生二维码的Android例子
- [开源中国]android客户端改造
- Android(安卓)程序开发的插件化 模块化方法 之一
- Android(安卓)Out Of Memory(OOM)内存溢出
- Android(安卓)Studio 中高德地图申请key和获取sha1及配置的几点
- 第七章 ARM 反汇编基础(三)(ARM 原生程序的生成过程)