自定义运行时注解、编译时注解[ButterKnife原理探析]

本篇博文针对具备注解基础的读者,主要讲解如何进行自定义注解。关于注解的具体基础知识点,网上这方面的学习资料非常多,可自行学习。

注解目前在主流的框架,比如Android中的Glide、Retrofit;Java Web方向的Spring等都有大量的使用。在给开发者带来巨大方便的同时,作为开发者有必要了解学习注解及其自定义,甚至可以自定义自己的注解库呦。

以下分析均在Android Studio中进行;

1、基础

自定义运行时注解、编译时注解[ButterKnife原理探析]_第1张图片
上图来源于网络。感谢。上图是对Java注解知识点的总结,已经十分的全面了。如果你只是想自定义一个运行时的注解及处理器,上述知识点已经足够。但是如果你想要自定义一个编译型注解,那你还需要学习更多东西,比如AbstractProcessor、APT、JavaPoet等等,稍后会介绍。

2、区别

运行时注解与编译时注解的区别是什么呢?
a)保留阶段不同。运行时注解保留到运行时,可在运行时访问。而编译时注解保留到编译时,运行时无法访问。
b)原理不同。运行时注解是Java反射机制,而编译时注解通过APT、AbstractProcessor。
c)性能不同。运行时注解由于使用Java反射,因此对性能上有影响。编译时注解对性能没影响。这也是为什么ButterKnife从运行时切换到了编译时的原因。
d)产物不同。运行时注解只需自定义注解处理器即可,不会产生其他文件。而编译时注解通常会产生新的Java源文件。

3、运行时注解

相对于编译时注解,运行时注解要简单的多。运行时注解的自定义只有两个步骤:
自定义注解 + 注解处理器(会用到java反射机制);

3.1、自定义运行时注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RuntimeBind {
    int value();
}
RetentionPolicy.RUNTIME:表明这是一个运行时的注解,如果是编译时注解,则应该是RetentionPolicy.CLASS。
ElementType.FIELD:表明该注解是用于域的。

3.2、自定义注解处理器

public class RuntimeAnnotationProcessor {
    public static void bind2(Activity activity){
        if (activity==null) return;
      Field[] fields =   activity.getClass().getDeclaredFields();
        if (fields == null || fields.length == 0) return;
        for(int i=0;i
可以看到,代码量还是很小的。首先,我们通过反射获取到当前Activity都有哪些字段,然后再判断每个字段是不是在使用RuntimeBind注解,如果使用了,则获取该注解的值,同时将该注解的值赋值给对应的字段就可以了。

3.3、使用运行时注解处理器

public class MainActivity extends AppCompatActivity {

    @RuntimeBind(R.id.tv_runtime)
    TextView tv_runtime;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
       RuntimeAnnotationProcessor.bind2(this);
        tv_runtime.setText("运行时注解成功");
    }
}
在使用的时候,只需要调用RuntimeAnnotationProcessor.bind2(..)即可完成繁琐的findViewById工作了。是不是很方便。但是,如果存在大量的通过运行时注解,则在一定程度上会影响程序的性能的。因此,编译时注解的优势就发挥出来了。

4、编译时注解

编译时注解总体结构:编译时注解 + 注解处理器(基于AbstractProcessor) + APT + JavaPoet(自定义Java源文件会用到)  + auto-service(处理器注册);

4.1、基础准备

a)什么是APT?
APT(Annotation Processing Tool)是javac内置的工具,用于在编译时期扫描和处理注解信息。它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码(调用注解处理器的Process方法生成源文件)。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。
b)APT插件(工具)
android-apt:由一位开发者自己开发的apt框架,源代码托管在这里,随着Android Gradle 插件 2.2 版本的发布,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt ,自此android-apt 作者在官网发表声明最新的Android Gradle插件现在已经支持annotationProcessor,并警告和或阻止android-apt ,并推荐大家使用 Android 官方插件annotationProcessor。
annotationProcessor:是google开发的内置框架,不需要引入,可以直接在build.gradle文件中使用。
c)JavaPoet:该库是由Android大神ButterKnife作者完成,目的是简化我们书写自动生成Java代码的工作量。https://github.com/square/javapoet
d)auto-service:编译时注解处理器需要注册到JVM中。在以前我们需要手动的配置,具体如下:在Java Library项目中,在resources资源文件夹下创建META-INF.services,然后在该路径下创建名为javax.annotation.processing.Processor的文件,在该文件中配置(处理器的完整路径,每行一个)需要启用的注解处理器。现在使用这个库,一个@autoservice注解就完成了注册,不要太方便。

4.2、编译时注解流程

自定义运行时注解、编译时注解[ButterKnife原理探析]_第2张图片
这个生成的Java源文件是干什么的呢?其实就是替完成findViewById、OnClick等工作部分的代码,这也是编译时注解的目标,即免去开发者书写这些繁琐且无用功的代码。这些代码会在后面被调用,我们熟悉的ButterKnife.bind(this),其实主要就是调用了这部分代码。

4.3、编译时注解

自定义运行时注解、编译时注解[ButterKnife原理探析]_第3张图片
这是我在本地写的一个关于编译时注解的项目,后面会给出项目链接。该项目分为四个模块:
annotation模块:Java Library,包含了我们所有的自定义注解。
annotation_processor:Java Library,包含了所有的自定义注解处理器。
annotation_api:Android Library,该模块是暴露给开发者的,由开发者调用暴露的Api完成绑定。
app:主项目。
如上四个模块的依赖关系是:
annotation_processor依赖annotation;
annotation_api依赖annotation;
app依赖annotation_api,同时还会在app的build.gradle中,添加apt project(':annotation_processor');
在这四个模块中,annotation_processor与annotation的流程在4.2小节中已经很清楚了。annotation_api起到了桥梁的作用,annotation_api的主要工作如下:
1、寻找Java源文件。Java源文件由annotation_processor生成,我们可以根据传入的参数,比如Activity,寻找annotation_processor生成的Java源文件。注意Java源文件的命名有一定的规则,例如 ,后缀依据编写者的爱好会有所不同。之所以存在这样的命名规则,是方便我们可以按照这种规则快速的找到Java源文件。
2、通过接口调用绑定方法。annotation_api与生成的Java源文件之间是通过接口来完成调用的。
public interface IInject {
    void inject(T t);
}
因此,我们生成的java源文件也必须实现这个接口。

4.3.1、自定义编译时注解【annotation模块】

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface Bind {
    int value();
}
注意:一定要是RetentionPolicy,CLASS才是编译时注解。

4.3.2、自定义注解处理器【annotation_processor】

编译时的注解处理器需要继承AbstractProcessor,并重写其process方法即可。有的同学可能会说,应该还有getSupportedAnnotationTypes、getSupportedSourceVersion方法。没错,是需要重写这两个方法,但是由于我们使用了auto_service库,因此,两个注解就搞定了,不用再去麻烦的重写这两个方法了。
方法 作用
init(ProcessingEnvironment processingEnv) 该方法有注解处理器自动调用,其中ProcessingEnvironment类提供了很多有用的工具类:Filter,Types,Elements,Messager等
getSupportedAnnotationTypes() 该方法返回字符串的集合表示该处理器用于处理那些注解
getSupportedSourceVersion() 该方法用来指定支持的java版本,一般来说我们都是支持到最新版本,因此直接返回SourceVersion.latestSupported()即可
process(Set annotations, RoundEnvironment roundEnv) 该方法是注解处理器处理注解的主要地方,我们需要在这里写扫描和处理注解的代码,以及最终生成的java文件。其中需要深入的是RoundEnvironment类,该用于查找出程序元素上使用的注解
各方法含义如上图所示。关于abstractProcessor,还有几个重要的概念需要说明一下:
1、Element 
  - VariableElement //一般代表成员变量
  - ExecutableElement //一般代表类中的方法
  - TypeElement //一般代表代表类
  - PackageElement //一般代表Package
2、RoundEnvironment
public interface RoundEnvironment {

    boolean processingOver();

     //上一轮注解处理器是否产生错误
    boolean errorRaised();

     //返回上一轮注解处理器生成的根元素
    Set getRootElements();

   //返回包含指定注解类型的元素的集合
    Set getElementsAnnotatedWith(TypeElement a);

    //返回包含指定注解类型的元素的集合
    Set getElementsAnnotatedWith(Class a);
}
3、 ProcessingEnvironment 
public interface ProcessingEnvironment {

    Map getOptions();

    //Messager用来报告错误,警告和其他提示信息
    Messager getMessager();

    //Filter用来创建新的源文件,class文件以及辅助文件
    Filer getFiler();

    //Elements中包含用于操作Element的工具方法
    Elements getElementUtils();

     //Types中包含用于操作TypeMirror的工具方法
    Types getTypeUtils();

    SourceVersion getSourceVersion();

    Locale getLocale();
}
好了,材料准备好了,可以开始做饭了。
@SupportedAnnotationTypes("com.example.Bind")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@AutoService(Processor.class)
public class BindAnnotationProcessor extends AbstractProcessor {

    private Elements elementsUtils;
    private Filer fileUtil;
    private Map annotationInfoMap;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementsUtils = processingEnv.getElementUtils();
        fileUtil = processingEnv.getFiler();
        annotationInfoMap = new HashMap<>();
    }

    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        Set eleSet = roundEnv.getElementsAnnotatedWith(Bind.class);
        for (Element ele : eleSet) {
            if (!checkIsField(ele)) {
                System.out.println("non filed is error inject ");
                continue;
            }
            if (checkIsPrivate(ele)) {
                System.out.println("filed cant not be private ");
                continue;
            }
            VariableElement variableElement = (VariableElement) ele;
            // full class name
            String className = ((TypeElement) variableElement.getEnclosingElement()).getQualifiedName().toString();
            AnnotationInfo annotationInfo = new AnnotationInfo(elementsUtils.getPackageOf(variableElement), (TypeElement) variableElement.getEnclosingElement());
            int value = variableElement.getAnnotation(Bind.class).value();
            annotationInfo.bindData.put(value, variableElement);

            annotationInfoMap.put(className, annotationInfo);
        }
        for (String key : annotationInfoMap.keySet()) {
            AnnotationInfo annotationInfo = annotationInfoMap.get(key);
            JavaFileObject sourceFile = null;
            try {
                sourceFile = fileUtil.createSourceFile(annotationInfo.classEle.getSimpleName().toString() + "$$Injector");
                Writer writer = sourceFile.openWriter();
                writer.write(generateJavaSourceFile(annotationInfo));
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    private boolean checkIsPrivate(Element ele) {
        if (ele.getModifiers().contains(Modifier.PRIVATE))
            return true;
        return false;
    }

    private boolean checkIsField(Element ele) {
        if (ele.getKind().isField())
            return true;
        return false;
    }

    public String generateJavaSourceFile(AnnotationInfo annotationInfo) {

        StringBuilder sb = new StringBuilder();
        sb.append("//auto generate do not modify\n");
        sb.append("package " + annotationInfo.packageEle.getQualifiedName().toString() + ";\n")
                .append("import android.app.Activity;\n")
                .append("import com.example.annotation_api.IInject;\n")
                .append("public class " + annotationInfo.classEle.getSimpleName().toString() + "$$Injector")
                .append(" implements IInject<" + annotationInfo.classEle.getQualifiedName() + "> {")
                .append("@Override\n")
                .append("public void inject(" + annotationInfo.classEle.getQualifiedName() + " activity) {");
        for (int key : annotationInfo.bindData.keySet()) {
            Element ele = annotationInfo.bindData.get(key);
            sb.append("activity." + ele.getSimpleName() + " = (" + ele.asType() + ")activity.findViewById(" + key + ");\n");
        }
        sb.append("}}\n");
        return sb.toString();
    }
}
@SupportedAnnotationTypes("com.example.Bind"):声明支持Bind注解;
@SupportedSourceVersion(SourceVersion.RELEASE_7):升值直接的JDK版本;
@AutoService(Processor.class):注册处理器至JVM;
主要的工作都是由process方法完成的,APT根据注解生成代码其实就是调用注解处理器的process方法完成的,生成的Java类会放在app/build/generated/source/apt/debug中。因为我们需要在app中使用生成的Java源文件,以完成绑定工作。所以annotation_processor生成的Java源文件必须在app模块中。这也是为什么在app的build.gradle中,添加apt project(':annotation_processor');
自定义运行时注解、编译时注解[ButterKnife原理探析]_第4张图片
我们获取使用了Bind注解的所有Element,然后遍历Element的集合,并提取注解的信息保存到AnnotationInfo中。
在注解处理器中,我们定义了一个Map annotationInfoMap;其中key为使用该注解的类的全路径名称,value为该类的所有注解信息。例如:MainActivity,则key为packageName.MainActivity,而Value则为一个AnnotationInfo实例,该实例保存了MainActivity中所有的注解信息。下面我们先看下这个我们自定义的AnnotationInfo:
public class AnnotationInfo {

    public Map bindData;
    public PackageElement packageEle;
    public TypeElement classEle;

    public AnnotationInfo(PackageElement packageName,TypeElement className){
        this.packageEle = packageName;
        this.classEle = className;
        bindData = new HashMap<>();
    }
}
Map:key为注解的值,比如R.id.tv_annotationtest;Value则为对应的元素,比如TextView tv_annotationtest;
这样我们可以总结在process方法中,类与AnnotationInfo的关系:
MainActivity - 1:1 - AnnotationInfo  - 1:N - 注解的值及被注解的元素;
生成的Java源文件(MainActivity$$Injector)的代码: 
package com.example.xxx.myannotations;
import com.example.annotation_api.IInject;
public class MainActivity$$Injector implements IInject {
    @Override
public void inject(com.example.xiuli.myannotations.MainActivity activity) {
        activity.tv_annotationtest = (android.widget.TextView)activity.findViewById(2131492944);
}
}

4.4、暴露Api供开发者使用【annotation_api】

在该模块,我们定义了一个Injector类,通过bind方法暴露给开发者:
public class Injector {
    private static String suffix = "$$Injector";
    public static void bind(Activity activity){
        if (activity==null)
            return;
        StringBuilder sb  = new StringBuilder();
        sb.append(activity.getClass().getCanonicalName());
        sb.append(suffix);
        //查找注解生成的Java文件是否存在
        if(!findAnnotationGenerateJavaFile(sb.toString()))
            return;
        try {
            //通过接口调用Java 类的绑定方法inject,将注解的值绑定到字段
            IInject inject = (IInject) Class.forName(sb.toString()).newInstance();
            inject.inject(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
    private static boolean findAnnotationGenerateJavaFile(String fullName){
        try {
            Class.forName(fullName);
        } catch (Exception e) {
            e.printStackTrace();
        return false;
        }
        return true;
    }
}
Injector的主要作用就是根据既定的规则寻找已生成的Java源文件,然后加载并调用生成的Java类中的绑定方法。

4.5、使用

public class MainActivity extends AppCompatActivity {

    @Bind(R.id.tv_annotationtest)
    TextView tv_annotationtest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Injector.bind(this);
        tv_annotationtest.setText("编译型注解成功");
    }
}

源码:https://github.com/sparkerandroid/MyAnnotations
感谢:
http://www.mamicode.com/info-detail-1743070.html
https://yq.aliyun.com/articles/59493#
https://juejin.im/entry/585fe4e61b69e600562147fa/view
http://blog.csdn.net/lmj623565791/article/details/51931859

你可能感兴趣的:(Android)