ButterKnife编译时注解探秘

654 阅读8分钟
原文链接: blog.csdn.net

安卓中很多有名的开源框架都运用了编译时注解,如ButterKnife,EventBus , Retrofit , GreenDao等。所以作为一个合格的安卓开发者,学会运用编译时注解是非常有必要的。

下面就仿照ButterKnife的view的注入写一个例子来走一遍编译时注解的流程。

第一步新建module

1、创建一个module起名为annotation作为注解类的module 用来保存我们写的注解类
2、创建一个java module 起名为annotation-processor (注意一定是java modeule 因为这个类中需要继承AbstractProcessor,AbstractProcessor是javax中的类正常的android sdk中是引用不到的

最后的工程目录:

这里写图片描述

在module的build.gradle中配置一下Java的使用版本

sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7

这边最好不要用1.8,因为只有android N支持java8,如果你写1.8之后,强制要你使用buildToolsVersion为24.0.0

第二步编写annotation中的代码

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
    int value();
}

@Retention 注解说明,表示这个注解可以保留到哪个阶段 有三种
(1)RetentionPolicy.SOURCE —— 这种类型的Annotations只在源代码级别保留,编译时就会被忽略
(2)RetentionPolicy.CLASS —— 这种类型的Annotations编译时被保留,在class文件中存在,但JVM将会忽略
(3)RetentionPolicy.RUNTIME —— 这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用.
所以编译时注解我们就选第二种啦,编译的时候生成代码,以前的一些注解框架比如xutil中的view注入的注解使用的是第三种运行时注解,可以保留到app运行时中,使用的时候通过注解和反射来实现功能。由于运行的时候使用反射的效率不是很好,所以现在就流行编译的时候做事情啦。
@Target 注解的作用目标 表示这个注解的作用域
@Target(ElementType.TYPE) //接口、类、枚举、注解
        @Target(ElementType.FIELD) //字段、枚举的常量
        @Target(ElementType.METHOD) //方法
        @Target(ElementType.PARAMETER) //方法参数
        @Target(ElementType.CONSTRUCTOR) //构造函数
        @Target(ElementType.LOCAL_VARIABLE)//局部变量
        @Target(ElementType.ANNOTATION_TYPE)//注解
        @Target(ElementType.PACKAGE) ///包

第三步编写 processor

我们都知道编译时注解的运用一般都是在编译的时候生成我们需要的java文件然后去引用,
如果要生成 .java 的文件需要用到JavaPoet (square公司开源的用来生成java文件)
需要在processor的build.gradle文件中加上依赖

compile 'com.squareup:javapoet:1.9.0'

JavaPoet提供了(TypeSpec)用于创建类或者接口,(FieldSpec)用来创建字段,(MethodSpec)用来创建方法和构造函数,(ParameterSpec)用来创建参数,(AnnotationSpec)用于创建注解。

如果要生成一个java文件,首先我们要知道我们哟啊生成的java文件是神马样的,用过ButterKnife的朋友应该都见过它生成的java文件类似下面

public class MainActivity_ViewBinding {
  public MainActivity_ViewBinding(MainActivity target, View source) {
    target.mTextView = (android.widget.TextView)source.findViewById(2131427422);
  }
}

Ok 下面就通过JavaPoet 的api将上面的一个类拼接出来

 package com.example;

import com.chs.annotation.MyAnnotation;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;

public class MyProcessor extends AbstractProcessor{
    private Filer filer;
    //初始化处理器
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        // Filer是个接口,支持通过注解处理器创建新文件
        filer = processingEnv.getFiler();
    }
    //处理器的主函数 处理注解
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
            //获取最里面的节点
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            String packageName = enclosingElement.getQualifiedName().toString();
            packageName =packageName .substring(0, packageName.lastIndexOf("."));
            String className = enclosingElement.getSimpleName().toString();
            String typeMirror = element.asType().toString();

            //注解的值
            int annotationValue = element.getAnnotation(MyAnnotation.class).value();
            String name = element.getSimpleName().toString();
            TypeName type = TypeName.get(enclosingElement.asType());//此元素定义的类型
            if (type instanceof ParameterizedTypeName) {
                type = ((ParameterizedTypeName) type).rawType;
            }

            System.out.println("typeMirror:"+typeMirror);//被注解的对象的类型
            System.out.println("packageName:"+packageName);//包名
            System.out.println("className:"+className);//类名
            System.out.println("annotationValue:"+annotationValue);//注解传过来的参数
            System.out.println("name:"+name);//被注解的对象的名字
            System.out.println("type:"+type);//当前被注解对象所在类的完整路径

            ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
              //创建代码块
            CodeBlock.Builder builder = CodeBlock.builder()
                    .add("target.$L = ", name);//$L是占位符,会把后面的name参数拼接大$L所在的地方
                builder.add("($L)source.findViewById($L)", typeMirror , annotationValue);

                // 创建main方法
              MethodSpec methodSpec = MethodSpec.constructorBuilder()
                      .addModifiers(Modifier.PUBLIC)
                      .addParameter(type,"target")
                      .addParameter(ClassName.get("android.view", "View"),"source")
                      .addStatement("$L", builder.build())
                      .build();

                // 创建类
                TypeSpec helloWorld = TypeSpec.classBuilder(bindingClassName.simpleName())
                        .addModifiers(Modifier.PUBLIC)
                        .addMethod(methodSpec)
                        .build();

                try {
                    // 生成文件
                    JavaFile javaFile = JavaFile.builder("com.chs.annotationtest", helloWorld)
                            .build();
                    // 将文件写出
                    javaFile.writeTo(System.out);
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
        return true;
    }
    //指定处理器处理哪个注解
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add(MyAnnotation.class.getCanonicalName());
        return annotataions;
    }
    //指定使用的java的版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

}

AbstractProcessor是编译时注解处理器的主要类,我们需要自定义自己的MyProcessor 集成AbstractProcessor,重写它里面的方法。

上面的类中的

init 方法是初始化注解处理器

process 方法是最重要的一个方法,里面是代码生成的主要逻辑 ,拼接代码的逻辑上面注释很清楚啦!不过这里只实现了一个对象的注解,正常的项目中不可能只有一个对象,知道了一个对象的使用多个对象按同样的方法循环拼接出来就好啦

getSupportedAnnotationTypes 方法是 指定处理器处理哪个注解

getSupportedSourceVersion 方法是 指定java的兼容版本

第四步

服务注册文件(META-INF/services/javax.annotation.processing.Processor)

1、在module annotation-processor 中main文件夹下创建resources文件夹
2、在resources资源文件夹下创建META-INF文件夹
3、然后在META-INF文件夹中创建services文件夹
4、然后在services文件夹下创建名为javax.annotation.processing.Processor的文件
5、在javax.annotation.processing.Processor文件中配置我们自己的处理器的完整引用 比如com.example.MyProcessor
这里写图片描述

到这里我们就可以在主文件中试验一下啦 首先把注解的module和注解器的module的依赖添加到主项目中的build.gradle中

    compile project(':annotation')
    compile project(':annotation-processor')

MainActivity中使用注解

public class MainActivity extends AppCompatActivity {
    @MyAnnotation(R.id.tv_hello)
    TextView mTextView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

上面注解处理器的MyProcessor 类中的process方法的最后输出现在是输出到控制台System.out

javaFile.writeTo(System.out);

所以现在我们编译一下项目就可以在控制台中看到打印了注意不是在logCat中看是在studio 右下角的Gradle Console中看

结果如下:

typeMirror:android.widget.TextView
packageName:com.chs.annotationtest
className:MainActivity
annotationValue:2131427422
name:mTextView
type:com.chs.annotationtest.MainActivity
package com.chs.annotationtest;

import android.view.View;

public class MainActivity_ViewBinding {
  public MainActivity_ViewBinding(MainActivity target, View source) {
    target.mTextView = (android.widget.TextView)source.findViewById(2131427422);
  }
}

上面的log信息包括MyProcessor 类中的process方法中我们所写的System.out.println所打印出来的 和最终的类。可以看到最终生成了我们想要的类。

第五步

Ok然后我们就可以去试一下这个类可不可以用啦。
(1)
首先我们得把这个类输出成一个文件,方法就是在MyProcessor 类中的init方法中找到Filer
Filer是个接口,支持通过注解处理器创建新文件

  @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        // Filer是个接口,支持通过注解处理器创建新文件
        filer = processingEnv.getFiler();
    }

(2)
然后在MyProcessor 类中的process方法中最后通过Filer输出

                     // 将文件写出
                    javaFile.writeTo(filer);

然后重新编译工程,就可以看到生成的文件了。位置:
这里写图片描述
打开文件可以看到内容:

package com.chs.annotationtest;

import android.view.View;

public class MainActivity_ViewBinding {
  public MainActivity_ViewBinding(MainActivity target, View source) {
    target.mTextView = (android.widget.TextView)source.findViewById(2131427422);
  }
}

然后就是把主程序中的MainActivity跟我们生成的MainActivity_ViewBinding类关联起来让MainActivity_ViewBinding初始化MainActivity中的注解了的对象。

根据我们创建类的时候的规则可以找到我们创建的类的名字,然后通过反射执行里面的方法。

public class MyAnnotationUtil {
    public static void bind(Activity activity) {
        //获取activity的decorView(根view)
        View view = activity.getWindow().getDecorView();
        String qualifiedName = activity.getClass().getName();

        //找到该activity对应的Bind类的名字
        String generateClass = qualifiedName + "_ViewBinding";
            //然后调用Bind类的构造方法,从而完成activity里view的初始化
        try {
            Class.forName(generateClass).getConstructor(activity.getClass(), View.class).newInstance(activity, view);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在MainActivity中调用


public class MainActivity extends AppCompatActivity {
    @MyAnnotation(R.id.tv_hello)
    TextView mTextView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyAnnotationUtil.bind(this);
        mTextView.setText("哈哈哈哈");
    }
}

运行项目可以看到手机上

这里写图片描述

成功啦!ButterKnife的工作原理是不是很清晰啦!

用过ButterKnife的朋友都知道我们引入依赖的时候是有两句话的:下面是最新版的ButterKnife

dependencies {
  compile 'com.jakewharton:butterknife:8.8.1'
  annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}

老版的ButterKnife 第二句话是

apt 'com.jakewharton:butterknife-compiler:8.4.0'

annotationProcessor 和 apt是神马鬼。它们是Android Studio中处理注解处理的插件 处理注解的工具 其实它俩的作用是一样的作用如下:

可以自动的帮你为生成的代码创建目录,使注解处理器生成的代码能被Android Studio正确的引用,让生成的代码编译到APK里面去
重点:只在编译的时候执行依赖的库,但是库最终不打包到apk中, 因为我们的annotation-processor module的作用就是生成java文件,我们的App中用到的是它生成的java文件,而对于它自身的代码当打包完的时候是没什么用的,所以不应该打包到apk中。

所以我们的主项目中的依赖就可以改为:

    compile project(':annotation')
    annotationProcessor project(':annotation-processor')

把控制器annotation-processor改为annotationProcessor 依赖,运行项目效果跟上面的一样,而这次我们的apk中会少了annotation-processor中代码,这样做无疑可以使apk得到了优化。

apt跟annotationProcessor 的作用是一样的,只不过apt是个人自由开发,比annotationProcessor 出来的早,后来谷歌开发了annotationProcessor, Android Gradle 插件 2.2 版本内置annotationProcessor比apt的使用比apt简单。与时俱进吗,所以apt怎么用就不说啦。

注解处理器的简化:

上面在写完注解处理器MyProcessor 之后进行了注解处理器的注册
服务注册文件(resources/META-INF/services/javax.annotation.processing.Processor)
这样创建很多个文件夹是不是很麻烦呀,没关系有个简单的方法。

现在把我们创建的resources/META-INF/services/javax.annotation.processing.Processor全部删掉,重新编译项目可以看到我们想要的java文件没有生成

然后给module annotation-processor 添加依赖

compile 'com.google.auto.service:auto-service:1.0-rc3'

然后在MyProcessor 类上添加注解

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor{}

然后在编译项目可以看到我们想要的 MainActivity_ViewBing.java文件又出来了,运行项目效果跟上面一样。

好啦,到此为止是不是发现ButterKnife的实现原理你已经明白了呢 ?哈哈哈哈哈 !