注解学习笔记

一、概述

在Android应用开发中,我们常常为了提升开发效率会选择使用一些基于注解的框架,但是由于反射造成一定运行效率的损耗,所以我们会更青睐于编译时注解的框架,例如:

  • butterknife免去我们编写View的初始化以及事件的注入的代码。
  • EventBus3方便我们实现组建间通讯。
  • fragmentargs轻松的为fragment添加参数信息,并提供创建方法。
  • ParcelableGenerator可实现自动将任意对象转换为Parcelable类型,方便对象传输。

类似的库还有非常多,大多这些的库都是为了自动帮我们完成日常编码中需要重复编写的部分(例如:每个Activity中的View都需要初始化,每个实现Parcelable接口的对象都需要编写很多固定写法的代码)。

这里并不是说上述框架就一定没有使用反射了,其实上述其中部分框架内部还是有部分实现是依赖于反射的,但是很少而且一般都做了缓存的处理,所以相对来说,效率影响很小。

但是在使用这类项目的时候,有时候出现错误会难以调试,主要原因还是很多用户并不了解这类框架其内部的原理,所以遇到问题时会消耗大量的时间去排查。

那么,于情于理,在编译时注解框架这么火的时刻,我们有理由去学习:如何编写一个机遇编译时注解的项目

首先,是为了了解其原理,这样在我们使用类似框架遇到问题的时候,能够找到正确的途径去排查问题;其次,我们如果有好的想法,发现某些代码需要重复创建,我们也可以自己来写个框架方便自己日常的编码,提升编码效率;最后也算是自身技术的提升。

注:以下使用IDE为Android Studio.

本文将以编写一个View注入的框架为线索,详细介绍编写此类框架的步骤。

  • 什么是注解
    • 注解分类
    • 注解作用分类
  • 元注解
  • Java内置注解
  • 自定义注解
    • 自定义注解实现及使用
    • 编译时注解
      • 注解处理器
        • 注解处理器基本代码
        • 注解处理器一般处理逻辑
  • 如何编写基于编译时注解的项目
    • 项目结构划分
    • 注解模块的实现
    • 注解处理器的实现
      • Glide遇坑记之分析
        • 收集信息
        • 生成代理类
        • 生成Java代码
    • API模块的实现
  • ButterKnife工作流程解析
    • ButterKnife
      有哪些优势?
    • ButterKnife工作流程
    • Java注解的工作流程
  • 参考资料

二、编写前的准备

在编写此类框架的时候,一般需要建立多个module,例如本文即将实现的例子:

  • ioc-annotation 用于存放注解等,Java模块
  • ioc-compiler 用于编写注解处理器,Java模块
  • ioc-api 用于给用户提供使用的API,本例为Andriod模块
  • ioc-sample 示例,本例为Andriod模块

那么除了示例以为,一般要建立3个module,module的名字你可以自己考虑,上述给出了一个简单的参考。当然如果条件允许的话,有的开发者喜欢将存放注解和API这两个module合并为一个module。

对于module间的依赖,因为编写注解处理器需要依赖相关注解,所以:

ioc-compiler依赖ioc-annotation

我们在使用的过程中,会用到注解以及相关API

所以ioc-sample依赖ioc-api;ioc-api依赖ioc-annotation

什么是注解

  • 在Java语法中,使用@符号作为开头,并在@后面紧跟注解名。被运用于类,接口,方法和字段之上。

  • 注解也叫元数据,是一种代码级别的说明,与类,接口。枚举是在用一个层次上,他可以声明在包,类,字段,方法,局部变量,方法参数等的前面,用来对这些变量进行说明,注释。注解可以提高代码的可读性,它可以向编译器,虚拟机等解释说明一些事情。降低项目的耦合度,自动生成Java代码,自动完成一些规律性的代码,减少开发者的工作量。

三、注解模块的实现

注解模块,主要用于存放一些注解类,本例是模板butterknife实现View注入,所以本例只需要一个注解类:

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

我们设置的保留策略为Class,注解用于Field上。这里我们需要在使用时传入一个id,直接以value的形式进行设置即可。

你在编写的时候,分析自己需要几个注解类,并且正确的设置@Target以及@Retention即可。

注解分类

  • Java内置注解
  • 元注解
  • 自定义注解
    • 运行时注解
    • 编译时注解

四、注解处理器的实现

定义完成注解后,就可以去编写注解处理器了,这块有点复杂,但是也算是有章可循的。

该模块,我们一般会依赖注解模块,以及可以使用一个auto-service

build.gradle的依赖情况如下:

dependencies {
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile project (':ioc-annotation')
}

auto-service库可以帮我们去生成META-INF等信息。

注解作用分类

  • 编写文档
    • 通过代码里标识的元数据生成文档【生成文档doc文档】
  • 代码分析
    • 通过代码里标识的元数据对代码进行分析【使用反射】
  • 编译检查
    • 通过代码里标识的元数据让编译器能够实现基本的编译检查【Override】

Java字段(类成员)和属性

  • 属性只局限于类中方法声明,并不与类中其他的成员相关

  • Java中的属性通常可以理解为get和set方法;而字段通常叫做类成员

  • 字段通常是在类中定义的类成员变量

(1)基本代码

注解处理器一般继承于AbstractProcessor,刚才我们说有章可循,是因为部分代码的写法基本是固定的,如下:

@AutoService(Processor.class)
public class IocProcessor extends AbstractProcessor{
    private Filer mFileUtils;
    private Elements mElementUtils;
    private Messager mMessager;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv){
        super.init(processingEnv);
        mFileUtils = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
    }
    @Override
    public Set<String> getSupportedAnnotationTypes(){
        Set<String> annotationTypes = new LinkedHashSet<String>();
        annotationTypes.add(BindView.class.getCanonicalName());
        return annotationTypes;
    }
    @Override
    public SourceVersion getSupportedSourceVersion(){
        return SourceVersion.latestSupported();
    }
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
    }

在实现AbstractProcessor后,process()方法是必须实现的,也是我们编写代码的核心部分,后面会介绍。

我们一般会实现getSupportedAnnotationTypes()getSupportedSourceVersion()两个方法,这两个方法一个返回支持的注解类型,一个返回支持的源码版本,参考上面的代码,写法基本是固定的。

除此以外,我们还会选择复写init()方法,该方法传入一个参数processingEnv,可以帮助我们去初始化一些父类类:

  • Filer mFileUtils; 跟文件相关的辅助类,生成JavaSourceCode.
  • Elements
    mElementUtils;跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
  • Messager mMessager;跟日志相关的辅助类。

这里简单提一下Elemnet,我们简单认识下它的几个子类,根据下面的注释,应该已经有了一个简单认知。

Element 
  - VariableElement //一般代表成员变量
  - ExecutableElement //一般代表类中的方法
  - TypeElement //一般代表代表类
  - PackageElement //一般代表Package

元注解(负责注解其他的注解)

  • @Target

    • 表示该注解用于什么地方,可能的ElementType参数包括:
      • CONSTRUCTOR:构造器的声明
      • FIELD:域声明
      • LOCAL_VARIABLE:局部变量声明
      • METHOD:方法声明
      • PACKAGE:包声明
      • PARAMETER:参数声明
      • TYPE:类,接口或enum声明
  • @Retention

    • 表示在什么级别保留此信息,可选的RetentionPolicy参数包括:
      • SOURCE:注解仅存在代码中,注解会被编译器丢弃
      • CLASS:注解会在class文件中保留,但会被VM丢弃
      • RUNTIME:VM运行期间也会保留该注解,因此可以通过反射来获得该注解
  • @Documented

    • 将注解包含在javadoc中
  • @Inherited

    • 允许子类继承父类的注解

(2)process的实现

process中的实现,相比较会比较复杂一点,一般你可以认为两个大步骤:

  • 收集信息
  • 生成代理类(本文把编译时生成的类叫代理类)

什么叫收集信息呢?就是根据你的注解声明,拿到对应的Element,然后获取到我们所需要的信息,这个信息肯定是为了后面生成JavaFileObject所准备的。

例如本例,我们会针对每一个类生成一个代理类,例如MainActivity我们会生成一个MainActivity$$ViewInjector。那么如果多个类中声明了注解,就对应了多个类,这里就需要:

  • 一个类对象,代表具体某个类的代理类生成的全部信息,本例中为ProxyInfo
  • 一个集合,存放上述类对象(到时候遍历生成代理类),本例中为Map<String, ProxyInfo>,key为类的全路径。

这里的描述有点模糊没关系,一会结合代码就好理解了。

Java内置注解

  • @Override,表示当前的方法定义将覆盖超类中的方法,如果出现错误,编译器就会报错。

    • 当我们的子类覆写父类中的方法的时候,我们使用这个注解,这一定程度的提高了程序的可读性也避免了维护中的一些问题,比如说,当修改父类方法签名(方法名和参数)的时候,你有很多个子类方法签名也必须修改,否则编译器就会报错,当你的类越来越多的时候,那么这个注解确实会帮上你的忙。如果你没有使用这个注解,那么你就很难追踪到这个问题。
  • @Deprecated:如果使用此注解,编译器会出现警告信息。

    • 一个弃用的元素(类,方法和字段)在java中表示不再重要,它表示了该元素将会被取代或者在将来被删除。
      当我们弃用(deprecate)某些元素的时候我们使用这个注解。所以当程序使用该弃用的元素的时候编译器会弹出警告。当然我们也需要在注释中使用@deprecated标签来标示该注解元素。
  • @SuppressWarnings:忽略编译器的警告信息

    • 当我们想让编译器忽略一些警告信息的时候,我们使用这个注解。比如在下面这个示例中,我们的deprecatedMethod()方法被标记了@Deprecated注解,所以编译器会报警告信息,但是我们使用了@SuppressWarnings(“deprecation”)也就让编译器不在报这个警告信息了

a.收集信息

private Map<String, ProxyInfo> mProxyMap = new HashMap<String, ProxyInfo>();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
    mProxyMap.clear();
    Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
    //一、收集信息
    for (Element element : elements){
        //检查element类型
        if (!checkAnnotationUseValid(element)){
            return false;
        }
        //field type
        VariableElement variableElement = (VariableElement) element;
        //class type
        TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();//TypeElement
        String qualifiedName = typeElement.getQualifiedName().toString();

        ProxyInfo proxyInfo = mProxyMap.get(qualifiedName);
        if (proxyInfo == null){
            proxyInfo = new ProxyInfo(mElementUtils, typeElement);
            mProxyMap.put(qualifiedName, proxyInfo);
        }
        BindView annotation = variableElement.getAnnotation(BindView.class);
        int id = annotation.value();
        proxyInfo.mInjectElements.put(id, variableElement);
    }
    return true;
}

首先我们调用一下mProxyMap.clear();,因为process可能会多次调用,避免生成重复的代理类,避免生成类的类名已存在异常。

然后,通过roundEnv.getElementsAnnotatedWith拿到我们通过@BindView注解的元素,这里返回值,按照我们的预期应该是VariableElement集合,因为我们用于成员变量上。

接下来for循环我们的元素,首先检查类型是否是VariableElement.

然后拿到对应的类信息TypeElement,继而生成ProxyInfo对象,这里通过一个mProxyMap进行检查,key为qualifiedName即类的全路径,如果没有生成才会去生成一个新的,ProxyInfo与类是一一对应的。

接下来,会将与该类对应的且被@BindView声明的VariableElement加入到ProxyInfo中去,key为我们声明时填写的id,即View的id。

这样就完成了信息的收集,收集完成信息后,应该就可以去生成代理类了。

自定义注解

  • 运行时注解大多数时候实时运行时使用反射来实现所需效果,这很大程度上影响效率
  • 编译时注解在编译时生成对应Java代码实现代码注入

b.生成代理类

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
    //...省略收集信息的代码,以及try,catch相关
    for(String key : mProxyMap.keySet()){
        ProxyInfo proxyInfo = mProxyMap.get(key);
        JavaFileObject sourceFile = mFileUtils.createSourceFile(
                proxyInfo.getProxyClassFullName(), proxyInfo.getTypeElement());
            Writer writer = sourceFile.openWriter();
            writer.write(proxyInfo.generateJavaCode());
            writer.flush();
            writer.close();
    }
    return true;
}

可以看到生成代理类的代码非常的简短,主要就是遍历我们的mProxyMap,然后取得每一个ProxyInfo,最后通过mFileUtils.createSourceFile来创建文件对象,类名为proxyInfo.getProxyClassFullName(),写入的内容为proxyInfo.generateJavaCode().

看来生成Java代码的方法都在ProxyInfo里面。

自定义注解实现及使用

自定义注解使用@interface来声明一个注解。创建一个自定义注解遵循:
public @interface 注解名 {方法参数}

自定义注解示例一

@Documented
@Target(ElementType.METHOD)
@Inherited                                                                                                                                                                                                                                                                                                                                                                           @Retention(RetentionPolicy.RUNTIME)
public @interface Annotation{                                                                                                                                 
    int studentAge() default 18;   //定义默认值
    String studentName();
    String stuAddress();
    String stuStream() default "CSE";
}

@Annotation(studentName = "Chaitanya", stuAddress = "Agra, India")
public class Class {                                                                                                                                                                   
    ...
}

自定义注解示例二

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface getViewTo {
    int value() default  -1;
}

public class MainActivity extends AppCompatActivity {

    @getViewTo(R.id.textview)
    private TextView mTv;

    /**
     * 解析注解,获取控件
     */
    private void getAllAnnotationView() {
        //获得成员变量
        Field[] fields = this.getClass().getDeclaredFields();

        for (Field field : fields) {
          try {
            //判断注解
            if (field.getAnnotations() != null) {
              //确定注解类型
              if (field.isAnnotationPresent(GetViewTo.class)) {
                //允许修改反射属性
                field.setAccessible(true);
                GetViewTo getViewTo = field.getAnnotation(GetViewTo.class);
                //findViewById将注解的id,找到View注入成员变量中
                field.set(this, findViewById(getViewTo.value()));
              }
            }
          } catch (Exception e) {
          }
        }
      }
}

c.生成Java代码

这里我们主要关注其生成Java代码的方式。

下面主要看生成Java代码的方法:

#ProxyInfo
//key为id,value为对应的成员变量
public Map<Integer, VariableElement> mInjectElements = new HashMap<Integer, VariableElement>();

public String generateJavaCode(){
    StringBuilder builder = new StringBuilder();
    builder.append("package " + mPackageName).append(";nn");
    builder.append("import com.zhy.ioc.*;n");
    builder.append("public class ").append(mProxyClassName).append(" implements " + SUFFIX + "<" + mTypeElement.getQualifiedName() + ">");
    builder.append("n{n");
    generateMethod(builder);
    builder.append("n}n");
    return builder.toString();
}
private void generateMethod(StringBuilder builder){
     builder.append("public void inject("+mTypeElement.getQualifiedName()+" host , Object object )");
    builder.append("n{n");
    for(int id : mInjectElements.keySet()){
        VariableElement variableElement = mInjectElements.get(id);
        String name = variableElement.getSimpleName().toString();
        String type = variableElement.asType().toString() ;

        builder.append(" if(object instanceof android.app.Activity)");
        builder.append("n{n");
        builder.append("host."+name).append(" = ");
        builder.append("("+type+")(((android.app.Activity)object).findViewById("+id+"));");
        builder.append("n}n").append("else").append("n{n");
        builder.append("host."+name).append(" = ");
        builder.append("("+type+")(((android.view.View)object).findViewById("+id+"));");
        builder.append("n}n");
    }
    builder.append("n}n");
}

这里主要就是靠收集到的信息,拼接完成的代理类对象了,看起来会比较头疼,不过我给出一个生成后的代码,对比着看会很多。

package com.zhy.ioc_sample;
import com.zhy.ioc.*;
public class MainActivity$$ViewInjector implements ViewInjector<com.zhy.ioc_sample.MainActivity>{
    @Override
    public void inject(com.zhy.sample.MainActivity host , Object object ){
        if(object instanceof android.app.Activity){
            host.mTv = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492945));
        }
        else{
            host.mTv = (android.widget.TextView)(((android.view.View)object).findViewById(2131492945));
        }
    }
}

这样对着上面代码看会好很多,其实就死根据收集到的成员变量(通过@BindView声明的),然后根据我们具体要实现的需求去生成java代码。

这里注意下,生成的代码实现了一个接口ViewInjector<T>,该接口是为了统一所有的代理类对象的类型,到时候我们需要强转代理类对象为该接口类型,调用其方法;接口是泛型,主要就是传入实际类对象,例如MainActivity,因为我们在生成代理类中的代码,实际上就是实际类.成员变量的方式进行访问,所以,使用编译时注解的成员变量一般都不允许private修饰符修饰(有的允许,但是需要提供getter,setter访问方法)。

这里采用了完全拼接的方式编写Java代码,你也可以使用一些开源库,来通过Java api的方式来生成代码,例如:javapoet.

A Java API for generating .java source files.

到这里我们就完成了代理类的生成,这里任何的注解处理器的编写方式基本都遵循着收集信息、生成代理类的步骤。

编译时注解

说到编译时注解,就不得不说注解处理器
AbstractProcessor
,如果你有注意,一般第三方注解相关的类库(基于注解的框架),如bufferKnike、ARouter,都有一个Compiler命名的Module,如下图X2.3,这里面一般都是注解处理器,用于编译时处理对应的注解。

注解处理器(Annotation
Processor)是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以对自定义注解注册相应的注解处理器,用于处理注解逻辑

javac是收录于JDK中的Java语言编译器。该工具可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于Java虚拟机的字节码。

五、API模块的实现

有了代理类之后,我们一般还会提供API供用户去访问,例如本例的访问入口是

//Activity中
 Ioc.inject(Activity);
 //Fragment中,获取ViewHolder中
 Ioc.inject(this, view);

模仿了butterknife,第一个参数为宿主对象,第二个参数为实际调用findViewById的对象;当然在Actiivty中,两个参数就一样了。

API一般如何编写呢?

其实很简单,只要你了解了其原理,这个API就干两件事:

  • 根据传入的host寻找我们生成的代理类:例如MainActivity->MainActity$$ViewInjector
  • 强转为统一的接口,调用接口提供的方法。

这两件事应该不复杂,第一件事是拼接代理类名,然后反射生成对象,第二件事强转调用。

public class Ioc{
    public static void inject(Activity activity){
        inject(activity , activity);
    }
    public static void inject(Object host , Object root){
        Class<?> clazz = host.getClass();
        String proxyClassFullName = clazz.getName()+"$$ViewInjector";
       //省略try,catch相关代码 
        Class<?> proxyClazz = Class.forName(proxyClassFullName);
        ViewInjector viewInjector = (com.zhy.ioc.ViewInjector) proxyClazz.newInstance();
        viewInjector.inject(host,root);
    }
}
public interface ViewInjector<T>{
    void inject(T t , Object object);
}

代码很简单,拼接代理类的全路径,然后通过newInstance生成实例,然后强转,调用代理类的inject方法。

这里一般情况会对生成的代理类做一下缓存处理,比如使用Map存储下,没有再生成,这里我们就不去做了。

这样我们就完成了一个编译时注解框架的编写。

注解处理器

实现一个自定义注解处理器,至少重写四个方法,并注册你的Processor(为自定义注解注册相应的注解处理器,用于处理注解逻辑)

  • @AutoService(Processor.class),谷歌提供的自动注册注解,为你生成注册Processor所需要的格式文件(com.google.auto相关包)。
  • init(ProcessingEnvironment env),初始化处理器,一般在这里获取我们需要的工具类。
  • getSupportedAnnotationTypes(),指定注解处理器是注册给哪个注解的,返回指定支持的注解类集合。
  • getSupportedSourceVersion() ,指定java版本。
  • process(),处理器实际处理逻辑入口。

六、总结

澳门新葡亰网站注册,本文通过具体的实例来描述了如何编写一个基于编译时注解的项目,主要步骤为:项目结构的划分、注解模块的实现、注解处理器的编写以及对外公布的API模块的编写。通过文本的学习应该能够了解基于编译时注解这类框架运行的原理,以及自己如何去编写这样一类框架。

源码地址: 

注解处理器基本代码

init()方法传入一个参数processingEnv,可以帮助我们去初始化一些辅助类:

  • Filer mFileUtils; 跟文件相关的辅助类,生成JavaSourceCode.
  • Elements
    mElementUtils;跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
  • Messager mMessager;跟日志相关的辅助类。
注解处理器一般处理逻辑

1、遍历得到源码中,需要解析的元素列表。
2、判断元素是否可见和符合要求。
3、组织数据结构得到输出类参数。
4、输入生成Java文件。
5、错误处理。

Processor处理过程中,会扫描全部Java源码,代码的每一个部分都是一个特定类型(比如类、变量、方法)的Element,它们像是XML一层的层级机构,比如类、变量、方法等,每个Element代表一个静态的、语言级别的构件。

Element代表的是源代码,而TypeElement代表的是源代码中的类型元素,例如类。然而,TypeElement并不包含类本身的信息。你可以从TypeElement中获取类的名字,但是你获取不到类的信息,例如它的父类。这种信息需要通过TypeMirror获取。你可以通过调用elements.asType()获取元素的TypeMirror。

Element 相关子类

  • VariableElement //一般代表成员变量
  • ExecutableElement //一般代表类中的方法
  • TypeElement //一般代表代表类
  • PackageElement //一般代表Package

如何编写基于编译时注解的项目

在Android应用开发中,我们常常为了提升开发效率会选择使用一些基于注解的框架,但是由于反射造成一定运行效率的损耗,所以我们会更青睐于编译时注解的框架,例如:

  • ButterKnife免去我们编写View的初始化以及事件的注入的代码。
  • EventBus3方便我们实现组建间通讯。
  • Fragmentargs轻松的为Fragment添加参数信息,并提供创建方法。
  • ParcelableGenerator可实现自动将任意对象转换为Parcelable类型,方便对象传输。