1. 前提

本博客就是为了让我自己更好的理解butterknife的原理,或者是更好的让大家学习一下运行时注解,所以本博客的大前提是在参考 张鸿洋 的 Android如何编写基于编译时注解的项目 编写的,但是 张鸿洋 大神有很多地方没有解释到,本篇文章可以让初学者,学习到怎样使用运行时注解.可以为大家更好的除去疑惑

2. 开始编写

2.1 框架分为四个模块,前三个为核心模块:

  1. butterknife-annotations: 自定义注解模块,Java Library类型模块
  2. butterknife-compiler: 注解处理器模块,用于处理注解并生成文件,Java Library类型模块
  3. butterknife-api: 框架api模块,供使用者调用,Android Library类型模块
  4. 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的时候遇到错误,下面总结一波错误:

  1. 编码GBK的不可映射字符的错误
    因为 如果AbstractProcessor中或者ProxyInfoClass有中文, 会影响i/o流写入文件,所以在每一个build.gralde中加入

        tasks.withType(JavaCompile) {        options.encoding = "UTF-8"    }
  2. 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 } }  }
  3. 如果MainActivity_ViewInject.java这个代理类中有错误
    请核对好 ProxyInfoClass.java 中的方法 和 注释

4.6 检验

在demo的Activity中写入demo,检验即可

demo地址 github

参考文章

Android 如何编写基于编译时注解的项目 (也可以说转载人家的)
利用APT实现Android编译时注解
深入理解ButterKnife源码并掌握原理(一)

更多相关文章

  1. Android新技术------Android(安卓)App Bundle之bundletool的使用
  2. Android中使用javah生成jni头文件的正确方法
  3. Android高级工程师BAT面试题及知识点整理大全(Java基础+Android模
  4. 产生二维码的Android例子
  5. [开源中国]android客户端改造
  6. Android(安卓)程序开发的插件化 模块化方法 之一
  7. Android(安卓)Out Of Memory(OOM)内存溢出
  8. Android(安卓)Studio 中高德地图申请key和获取sha1及配置的几点
  9. 第七章 ARM 反汇编基础(三)(ARM 原生程序的生成过程)

随机推荐

  1. Android(安卓)滚动条属性
  2. 下载android sdk, adt
  3. Android(安卓)监听home键(android:launchM
  4. Android超链接去下划线--Android学习笔记
  5. Android(安卓)技术用于汇总
  6. Android(安卓)Edittext焦点处理;
  7. moodle手机版在android平台上的安装配置
  8. 在Eclipse中导入android sdk源码
  9. Android教程之Android(安卓)SDK1.5模拟器
  10. Android(安卓)Studio 更新问题