首页 > 代码库 > 自定义注解框架的那些事

自定义注解框架的那些事

一、前言

距离上次更新已过一个半月,工作太忙也不是停更的理由。我这方面做得很不好,希望大家给予监督。首先会讲解【编译期资源注入】,接着是【下拉刷新注入】(通过注解来实现下拉刷新功能),最后打造一款【特色的注解框架】。

大家准备好公交卡了吗,开车了 …

技术分享

二、什么是注解

每位童鞋对 注解 都有自己的理解,字面上的意思就是【额外的加入】,在项目当中使用的注解的框架已经越来越多,如 : retrofit ,butterknife,androidannotations … 2017年Android百大框架排行榜 基本都会看到注解的身影 。

注解可以减少大量重复的工作,提高开发效率,注解也非常的灵活,可以注解到 类、方法、属性等 当中,用来自动完成一些规律性的代码,以及降低类与类之间的耦合度。

前一篇文章对运行时RUNTIME事件的注解做了一个简单的讲解 初谈Android-Annotations(二) , 本篇主要简单讲解编译时CLASS**res**文件下的资源注入。前者一般是通过反射来实现的,影响效率,接下来一起来了解下编译时CLASS的实现。

三、编译时的注解

类似 butterknife,androidannotations,arouter 使用的都是编译时的注解CLASS。这里以 butterknife 为例,来看一看以下注解:

/**
 * Bind a field to the specified string resource ID.
 * <pre><code>
 * {@literal @}BindString(R.string.username_error) String usernameErrorText;
 * </code></pre>
 */
@Retention(CLASS) @Target(FIELD) //编译时注解  针对属性(成员变量)注解
public @interface BindString {
  /** String resource ID to which the field will be bound. */
  @StringRes int value();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

如上定义了资源字符串的注解,那么怎么去实现字符串的注入的呢?

这里就不得不提到注解处理器 AbstractProcessor,注解处理器是 javac 的一个工具,它用来在编译时扫描和处理注解(Annotation)。以编译过的字节码作为输入,生成文件(一般是使用 javapoet 生成 .Java 文件)作为输出,生成的 Java 文件一样被 javac 编译。同样运行时注解 (RetentionPolicy.RUNTIME) 和源码注解 (RetentionPolicy.SOURCE) 也可以在注解处理器进行处理。

注意:通过注解处理器处理注解是不能修改已经存在的 Java 类。例如增删一些方法。

四、字符串资源的注入

颜色(color),数组,尺寸的注入与字符串的注入类似,这里以字符串的注入来讲解。

先看看最终的实现效果:

技术分享

log的打印日志:

技术分享

通过日志可以看出:前面两个属性并没有在注解中引用字符串的资源,最终的结果和引用 R.string.app_name 的结果一样,这样可以节省代码量(为懒人服务)。目前支持驼峰式命名与前缀m命名,规则由你改写,随性打造属于你自己的注入框架。

基础知识科普

如下所示,实现一个自定义注解处理器,至少重写四个方法,并且注册你的自定义Processor

  • @AutoService(Processor.class),谷歌提供的自动注册注解,为你生成注册Processor所需要的格式文件。请在当前库的 gradle 文件添加如下依赖:
dependencies {
    compile fileTree(dir: ‘libs‘, include: [‘*.jar‘])
    compile ‘com.google.auto.service:auto-service:1.0-rc3‘//添加
    compile ‘com.google.auto:auto-common:0.8‘//添加
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
  • init(ProcessingEnvironment processingEnv) 初始化处理器,一般在这里获取我们需要的工具类

  • getSupportedAnnotationTypes() 返回注解器所支持的注解类型集合

  • getSupportedSourceVersion 返回Java版本

  • process 当于每个处理器的主函数main(),处理注解的逻辑(扫描、检验,以及生成Java文件),如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们,反之则要求后续 Processor 处理它们

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

    private Types typeUtils; //类型工具
    private Elements elementUtils;//元素工具
    private Filer filer; //文件管理器
    private Messager messager;//处理异常

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        typeUtils = processingEnv.getTypeUtils();//获取类的信息
        elementUtils = processingEnv.getElementUtils();//获取程序的元素 如 包 类 方法
        filer = processingEnv.getFiler();//生成java文件
        messager = processingEnv.getMessager();//处理错误日志
    }

     /** 
     * @param annotations   请求处理的注解类型 
     * @param roundEnv  有关当前和以前的信息环境 
     * @return 
     */  
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }



    /**
     * @return 返回的是 注解类型的合法全称集合,如果没有这样的类型,则返回一个空集合
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<String>();
        annotations.add(BindString.class.getCanonicalName());
        return annotations;
    }

    /**
     * 
     * @return 通常返回SourceVersion.latestSupported()
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

首先,我们梳理一下注解处理器的处理逻辑:

  • 遍历备注解的元素

  • 检验元素是否符合要求

  • 获取输出类参数

  • 生成 java 文件

  • 错误处理

接着来看个简单示例获取被注解的元素名称,与注解值:

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // roundEnv.getElementsAnnotatedWith()返回使用给定注解类型的元素
        for (Element element : roundEnv.getElementsAnnotatedWith(BindString.class)) {
            System.out.println("----------------------");
            // 判断元素的类型为Class
            if (element.getKind() == ElementKind.FIELD) {
                // 显示转换元素类型
                VariableElement  variableElement= (VariableElement) element;
                // 输出元素名称
                System.out.println(""+variableElement.getSimpleName());
                // 输出注解属性值
                System.out.println(""+variableElement.getAnnotation(BindString.class).value());
            }
            System.out.println("----------------------");
        }
        return false;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

MainActivity 类:

    @BindString(R.string.app_name)
    String appName;
  • 1
  • 2
  • 1
  • 2

Gradle console 控制台输出如下:

技术分享

看到这里,你一定会想,怎么样才能使 appName 与 2131099681建立联系呢?

如果不使用注解是这样来建立联系的:

    appName = getResources().getString(2131099681); // R.string.app_name =  2131099681
  • 1
  • 1

说一万道一千,实现这行代码的自动注入是我们的最终目的。

最后,我们来理解一下 Element 的概念,因为它是我们获取注解的基础。

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

Element 有五个直接子接口,它们分别代表一种特定类型的元素,如下:

  • PackageElement 表示一个包程序元素

  • TypeElement 表示一个类,接口或枚举程序元素 类型

  • VariableElement 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数 属性

  • ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序方法(静态或实例),包括注解类型元素 方法

  • TypeParameterElement 表示一般类、接口、方法或构造方法元素的泛型参数 如 public class MainActivity<T> 泛型 T

在开发中Element可根据实际情况强转为以上5种中的一种,它们都带有各自独有的方法,来看个简单的例子:

package com.github.wsannotation; // PackageElement

import java.util.List;

/**
 * desc:
 * author: wens
 * date: 2017/8/11.
 */

public class UserBean      // TypeElement
        <T extends List> {  // TypeParameterElement

    private int age;           // VariableElement
    private String name;        // VariableElement

    public UserBean() {          // ExecutableElement
    }

    public void setName(String name) {  // ExecutableElement
       String weight;               // VariableElement
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

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

相关文章链接:

自定义注解之编译时注解(RetentionPolicy.CLASS)(三)—— 常用接口介绍

字符串注入流程

这里参考了 ButterKnife 结合 androidannotations 的实现方式:

1、对元素效验 verify 系列方法

2、获取注解字段名和注解的资源ID以及处理资源ID为 -1 的情况

处理资源ID为 -1 的情况,我这里借鉴了 androidannotations 的处理方式:

        //处理默认情况
        if (resId == -1) {
            TypeElement androidRType = elementUtils.getTypeElement("com.github.butterknifelib.R.string");
            List<? extends Element> idEnclosedElements = androidRType.getEnclosedElements();
            List<VariableElement> idFields = ElementFilter.fieldsIn(idEnclosedElements);
            for (VariableElement idField : idFields) {
                TypeKind fieldType = idField.asType().getKind();
                if (fieldType.isPrimitive() && fieldType.equals(TypeKind.INT)) {
                //制定规则
                    if (idField.getSimpleName().toString().toLowerCase().replaceAll("_", "")
                            .equals(name.startsWith("m") ? name.substring(1, name.length()).toLowerCase() : name.toLowerCase())) {
                        resId = (int) idField.getConstantValue();
                        break;
                    }
                }
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

更好的方式是以 map 键值对来存储资源类 。后期我会做详细讲解。

3、生成 xxxx$$ViewBinder.java (xxxx 代码 Activity、View、Dialog)文件

4、给生成 xxxx$$ViewBinder.java 添加资源信息

5、通过 ButterKnife.bind(this); 反射得到 xxxx$$ViewBinder.java 类,并调用注入资源的方法

自定义注解框架的那些事