【Android】Android进阶之自定义注解

由于自己之前常用到的很多开源框架比如GreenDao、EventBus、ButterKnife、ARouter等都用到了自定义的注解,感觉自己应该花时间去学一下怎么在自己写的库中用到注解了,因此写下了这样一篇文章。

什么是注解

首先,要明白什么是注解。

An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.

注解是一种元数据,能添加到Java的代码中。类、方法、变量、参数、包都是可以添加注解的,注解对注解的代码并不会有直接的影响,仅仅是一个标记。之所以它能产生作用是因为对它解析之后做了一些处理,如ButterKnife在解析后自动为我们生成了一些代码。

注解是一个非常实用的工具,它有如下的作用:

  • 降低项目的耦合度。
  • 自动完成一些规律性的代码。
  • 自动生成java代码,减轻开发者的工作量。

元注解

Java中有一些常用的注解比如我们非常熟悉的Override、SuppressWarnings等等,我们可以从我们最常用的@Override入手,首先我们进入它的源码。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

可以看到,Override的源码非常简单,它上方有两个注解 @Target 以及 @Retention 。它们两个就是所谓的元注解。而 @interface 则是定义注解的关键字。

元注解,简单点说就是用来定义注解的注解。用于定义注解的作用范围,在什么元素上面等等信息。

元注解共有四种

  • @Retention: 保留的范围,默认值为CLASS,有下面三个值
    • SOURCE: 只在源码中可用
    • CLASS: 只在源码和字节码中可用
    • RUNTIME: 源码、字节码、运行时都可用
  • @Target: 用于表示可修饰哪些元素,有下面的几种值
    • TYPE:类、接口、枚举、注解类型。
    • FIELD:类成员(构造方法、方法、成员变量)。
    • METHOD:方法。
    • PARAMETER:参数。
    • CONSTRUCTOR:构造器。
    • LOCAL_VARIABLE:局部变量。
    • ANNOTATION_TYPE:注解。
    • PACKAGE:包声明。
    • TYPE_PARAMETER:类型参数。
    • TYPE_USE:类型使用声明。
  • @Inherited: 表示是否可以被继承,默认false
  • @Documented: 是否会添加到Javadoc文档中

@Retention是定义保留策略,决定了我们用何种方式解析。

SOURCE级别是用于标记的,而我们真正使用的往往是 CLASS(编译时) 和 RUNTIME(运行时) 。

自定义注解

下面我们写一个自定义注解@MyAnnotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyAnnotation {
    String value() default "N0tExpectErr0r";
}

上面可以看到,这个注解是一个运行期,用于修饰FIELD的注解。

而需要解释的是注解内部,定义了两个注解值。

注解值的写法如下:

类型 参数名() [default 默认值];

当参数名为value的时候,我们只需如@MyAnnotation("AAA")这样使用即可。

但当参数名不是value的时候,我们需要如@MyAnnotation(data = "AAA")这样使用。

运行时注解的处理

@Retention 的值为 RetentionPolicy.RUNTIME 的时候,它会保留到运行期,此时我们可以用反射解析注解。

public class AnnotationDemo {

    @MyAnnotation("AnnotationTest")
    private String testStr;
    @MyAnnotation()
    private String testStr2;

    public static void main(String[] args) {
        try {
            // 获取要解析的类
            Class cls = Class.forName("AnnotationDemo");
            // 获取所有Field
            Field[] declaredFields = cls.getDeclaredFields();
            for(Field field : declaredFields){
                // 获取Field上的注解
                MyAnnotation annotation = field.getAnnotation(MyAnnotation.class);
                if(annotation != null){
                    // 获取注解值
                    String value = annotation.value();
                    System.out.println(value);
                }

            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

我们看到打印结果:

AnnotationTest
N0tExpectErr0r

可以看到,我们指定value的值的时候,它使用了我们指定的值,当我们没有指定value的值的时候,它使用了value的默认值”N0tExpectErr0r”。

除了Field,其他的如类、方法的注解也是这样使用。

编译时注解的处理

其实像ButterKnife类似的功能可以用上面反射的方法在运行时实现,比如下面这段Demo

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

虽然上述代码可以实现类似ButterKnife的效果,但ButterKnife并不是这样干的。

为什么呢?因为反射非常影响程序的效率。

ButterKnife实际上是通过编译时注解,在编译的时候生成对应的java代码,从而实现注入。

@Retention(CLASS)
@Target(FIELD)
public @interface BindView{
    @IdRes int value();    
}

提到编译时注解,我们就需要提到注解处理器(Annotation Processor)。它是javac的一个工具,用于在编译时扫描和处理注解。

要自定义一个注解处理器,我们需要至少重写四个方法,并且注册自定义的Processor。

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

比如我们下面的这个CustomProcessor的例子:

@AutoService(Processor.class)
public class CustomProcessor extends AbstractProcessor {    
    /**
     * 注解处理器的初始化
     * 一般在这里获取我们需要的工具类
     * @param processingEnvironment 提供工具类Elements, Types和Filer
     */
    @Override
    public synchronized void init(ProcessingEnvironment env){ 
        super.init(env);        
        // Element代表程序的元素,例如包、类、方法。
        mElementUtils = env.getElementUtils();        
        // 处理TypeMirror的工具类,用于取类信息
        mTypeUtils = env.getTypeUtils();         
        // Filer可以创建文件
        mFiler = env.getFiler();        
        // 错误处理工具
        mMessages = env.getMessager();
    }    

    /**
     * 处理器实际处理逻辑入口
     * @param set
     * @param roundEnvironment 所有注解的集合
     * @return 
     */
    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) {        
        // 处理
    }    

    // 指定注解处理器是注册给哪个注解的,返回指定支持的注解类集合。
    @Override
    public Set<String> getSupportedAnnotationTypes() { 
          Set<String> sets = new LinkedHashSet<String>();          
          // 大部分class的getName、getCanonicalName这两个方法没有什么不同。
          // 但是对于array或内部类等就不同了。
          // getName返回的是[[Ljava.lang.String之类的表现形式,
          // getCanonicalName返回的就是跟我们声明类似的形式。
          sets(BindView.class.getCanonicalName());          
          return sets;
    }    

    // 指定Java版本,一般返回最新版本即可
    @Override
    public SourceVersion getSupportedSourceVersion() {        
        return SourceVersion.latestSupported();
    }
}

一般处理器的处理逻辑如下:

  1. 遍历得到源码中需要解析的元素列表。

  2. 判断元素是否可见和符合要求。

  3. 组织数据结构得到输出类参数。

  4. 输入生成java文件。

  5. 处理错误。

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

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

了解了Element之后,我们便能通过process 中的RoundEnvironment去获取扫描到的所有元素,如下面的代码,通过env.getElementsAnnotatedWith方法,获取被@BindView注解的元素的列表,用validateElement`校验元素是否可用。

@Override 
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env){
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
        if (!SuperficialValidation.validateElement(element))
            continue;
        try {
            praseResourceArray(element, builderMap, erasedTargetNames);
        } catch(Exception e) {
            logParsingError(element, BindArray.class, e);
        }
    }
}

由于env.getElementsAnnotatedWith返回的是所有被注解了@BindView 的元素。所以有时候我们还需要一些额外的判断,比如检查这些Element是否是一个类。

javapoet (com.squareup:javapoet)是一个根据指定参数,生成java文件的开源库,在处理器中按照参数创建出 JavaFile之后,通过Filer利用javaFile.writeTo(filer);就可以生成需要的java文件。

在处理器中我们不能直接抛出一个异常,因为在process()中抛出一个异常会导致运行注解处理器的JVM崩溃。因此,注解处理器有一个Messager类,一般通过messager.printMessage(Diagnostic.Kind.ERROR, StringMessage, element)即可正常输出错误信息。

在Android中使用自定义注解

这里写了一个Demo,创建了一个ViewAnnotation的Java Library。

gradle配置

首先,在这个Module的 build.gradle 中implementation了对应的库

apply plugin: 'java-library'

sourceCompatibility = "7"
targetCompatibility = "7"

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.google.auto.service:auto-service:1.0-rc4'
    implementation 'com.squareup:javapoet:1.11.1'
}

定义注解

然后,我定义了两个注解DIActivityDIView

DIActivity标明了要绑定View的id的Activity

DIView则用于指定控件对应的id

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface DIActivity{
}

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

实现注解处理器

最后,我自定义了一个名为DIProcessor的类用于解析Annotation并生成对应java文件

@AutoService(Processor.class)
public class DIProcessor extends AbstractProcessor {

    private Elements elementUtils;

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        // 规定需要处理的注解
        return Collections.singleton(DIActivity.class.getCanonicalName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.println("DIProcessor");
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(DIActivity.class);
        for (Element element : elements) {
            // 判断是否Class
            TypeElement typeElement = (TypeElement) element;
            List<? extends Element> members = elementUtils.getAllMembers(typeElement);
            MethodSpec.Builder bindViewMethodSpecBuilder = MethodSpec.methodBuilder("bindView")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(TypeName.VOID)
                    .addParameter(ClassName.get(typeElement.asType()), "activity");
            for (Element item : members) {
                DIView diView = item.getAnnotation(DIView.class);
                if (diView == null){
                    continue;
                }
                bindViewMethodSpecBuilder.addStatement(String.format("activity.%s = (%s) activity.findViewById(%s)",item.getSimpleName(),ClassName.get(item.asType()).toString(),diView.value()));
            }
            TypeSpec typeSpec = TypeSpec.classBuilder("DI" + element.getSimpleName())
                    .superclass(TypeName.get(typeElement.asType()))
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(bindViewMethodSpecBuilder.build())
                    .build();
            JavaFile javaFile = JavaFile.builder(getPackageName(typeElement), typeSpec).build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    private String getPackageName(TypeElement type) {
        return elementUtils.getPackageOf(type).getQualifiedName().toString();
    }
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
    }
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

可以看到,在生成的java文件里面我创建了一个名为bindView的方法,它是public void static的,并且参数是@DIActivity这个注解所标识的Activity的类名。之后,在bindView方法中添加了所有被 @DIView 标识的View的 findViewById 代码。

最后,将这个bindView方法放入了一个名为DI+对应Activity名称的final类中,然后通过JavaFile.builder方法创建了对应的JavaFile,然后调用JavaFile::writeTo方法创建对应的Java文件。

在项目中使用

当我们想要在项目中使用的时候,gradle的配置要分为老版本和新版本的gradle来处理

老版本

首先需要在Project的build.gradle中添加如下的配置:

dependencies {
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}

之后需要在app的build.gradle中添加如下配置:

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'

dependencies {
    implementation project(':ViewAnnotation')
    apt project(':ViewAnnotation')
}

新版本

新版本下,使用老版本的方式会报错

android-apt plugin is incompatible with future version of Android Gradle plugin. use ‘annotationProcessor’ configuration instead.

也就是说新版本的Gradle已经全面使用annotationProcessor来替代了apt

我们只需要按照如下的设置app的build.gradle即可

apply plugin: 'com.android.application'

dependencies {
    implementation project(':ViewAnnotation')
    annotationProcessor project(':ViewAnnotation')
}

在项目中使用

配置完gradle之后,我们便可以在项目中使用了

@DIActivity
public class MainActivity extends AppCompatActivity {

    @DIView(R.id.tv_text)
    TextView mTvText;
    @DIView(R.id.btn_change)
    Button mBtnChange;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        DIMainActivity.bindView(this);
        mTvText.setText("Test");
        mBtnChange.setOnClickListener(view->mTvText.setText("N0tExpectErr0r"));
    }
}

build之后我们可以看到编译期自动为我们生成的代码

public final class DIMainActivity extends MainActivity {
  public static void bindView(MainActivity activity) {
    activity.mTvText = (android.widget.TextView) activity.findViewById(2131165326);
    activity.mBtnChange = (android.widget.Button) activity.findViewById(2131165218);
  }
}

可以看到,和我们之前代码中的描述相同。

ButterKnife的流程

最后简单描述一下ButterKnife的 @BindView 注解生成代码的实现流程。

  1. @BindView在编译时,根据XXXAcitvity生成了XXXActivity$$ViewBinder
  2. 根据Activity中调用的ButterKnife.bind(this);通过this的类名+$$ViewBinder,反射得到了ViewBinder,和编译处理器生产的java文件关联起来,并将其存在map中缓存,然后调用ViewBinder.bind()
  3. 在ViewBinder的bind方法中通过id,利用ButterKnife的butterknife.internal.Utils工具类中的封装方法,将findViewById()控件注入到Activity的参数中。

说不定过两天可以做一下ButterKnife具体的源码解析。

N0tExpectErr0r

N0tExpectErr0r

一名热爱代码的 Android 开发者

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>