编译时注解之butterknife的简单实现

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-annotation
    butterknife-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
 */
@Override
public synchronized void init(ProcessingEnvironment processingEnv){
    super.init(processingEnv);

    //跟文件相关的辅助类,生成JavaSourceCode.
    mFileUtils = processingEnv.getFiler();
    //跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
    mElementUtils = processingEnv.getElementUtils();
    //跟日志相关的辅助类
    mMessager = processingEnv.getMessager();
}

/**
 * 返回支持的注解类型  一般都是固定写法一般都是固定写法
 * @return
 */
@Override
public 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
 */
@Override
public SourceVersion getSupportedSourceVersion(){
    return SourceVersion.latestSupported();
}


/**作用:
 * 1.收集信息
 *  就是根据你的注解声明,拿到对应的Element,然后获取到我们所需要的信息,这个信息肯定是为了后面生成JavaFileObject所准备的
 * 2 .生成代理类(本文把编译时生成的类叫代理类)
 * 我们会针对每一个类生成一个代理类,例如MainActivity我们会生成一个MainActivity$$ViewInjector
 *
 *
 * @param set
 * @param roundEnvironment
 * @return
 */
@Override
public boolean process(Set set, RoundEnvironment roundEnvironment) {
    //因为process可能会多次调用,避免生成重复的代理类,避免生成类的类名已存在异常
    mProxyMap.clear();
    // 通过roundEnvironment.getElementsAnnotatedWith拿到我们通过@BindView注解的元素,这里返回值,按照我们的预期应该是VariableElement集合,因为我们用于成员变量上。
    Set 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 { // TypeElement
    private int name; // VariableElement
    public 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源码并掌握原理(一)

你可能感兴趣的:(android,butterknife,运行时注解,android)