Android中注解以及Android APT的基本使用

一、什么是注解?

注解是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。通过使用注解,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。代码分析工具、 开发工具和部署工具可以通过这些补充信息进行验证、处理或者进行部署。

二、注解的分类

  • 标准注解
注解描述
@Override对覆盖超类中的方法进行标记,如果被标记的方法并没有实际覆盖超类中的方法,则编 译器会发出错误警告
@Deprecated对不鼓励使用或者已过时的方法添加注解,当编程人员使用这些方法时,将会在编译 时显示提示信息
@SuppressWarnings选择性地取消特定代码段中的警告
@SafeVarargsJDK 7新增,用来声明使用了可变长度参数的方法,其在与泛型类一起使用时不会出现 类型安全问题

除了标准注解,还有元注解,它用来注解其他注解,从而创建新的注解

  • 元注解
注解描述
@Targe注解所修饰的对象范围
@Inherited表示注解可以被继承
@Documented表示这个注解应该被JavaDoc工具记录
@Retention用来声明注解的保留策略
@RepeatableJDK 8 新增,允许一个注解在同一声明类型(类、属性或方法)上多次使用

其中@Targe注解取值是一个ElementType类型的数组,其中有以下几种取值,对应不同的对象范围

  • ElementType.TYPE:能修饰类、接口或枚举类型。
  • ElementType.FIELD:能修饰成员变量。
  • ElementType.METHOD:能修饰方法。
  • ElementType.PARAMETER:能修饰参数。
  • ElementType.CONSTRUCTOR:能修饰构造方法。
  • ElementType.LOCAL_VARIABLE:能修饰局部变量。
  • ElementType.ANNOTATION_TYPE:能修饰注解。
  • ElementType.PACKAGE:能修饰包。
  • ElementType.TYPE_PARAMETER:类型参数声明。
  • ElementType.TYPE_USE:使用类型

其中@Retention注解有3种类型,分别表示不同级别的保留策略。

  • RetentionPolicy.SOURCE:源码级注解。注解信息只会保留在.java源码中,源码在编译后,注解信息被丢弃,不会保留在.class中。
  • RetentionPolicy.CLASS:编译时注解。注解信息会保留在.java 源码以及.class 中。当运行Java程序时, JVM会丢弃该注解信息,不会保留在JVM中
  • RetentionPolicy.RUNTIME:运行时注解。当运行Java程序时,JVM也会保留该注解信息,可以通过反射获取该注解信息

三、注解的定义

基本定义

定义新的注解类型使用@interface关键字,这与定义一个接口很像,如下所示

1
2
3
public @interface Test{
  ....
}

定义完注解后则可以直接使用

1
2
3
4
@Test
public class AnnotationTest{
  ....
}

定义成员变量

注解只有成员变量,没有方法。注解的成员变量在注解定义中以“无形参的方法”形式来声明,其“方法名”定义了该成员变量的名字,其返回值定义了该成员变量的类型:

1
2
3
4
public @interface Test {
    String name();
    int age();
}

也可以在定义注解的成员变量时,使用default关键字为其指定默认值,如下所示:

1
2
3
4
public @interface Test {
    String name() default "郭翰林";
    int age() default 28;
}

定义注解的范围

定义注解的范围主要由上面的几个元注解来限定使用范围。下面这个示例则限制了这个注解只能去标记变量,并且此注解是编译时注解。

1
2
3
4
5
6
7
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.FIELD)
public @interface Test {
    String name() default "郭翰林";
    int age() default 28;
}

四、注解处理器

如果没有处理注解的工具,那么注解也不会有什么大的作用。对于不同的注解有不同的注解处理器。 虽然注解处理器的编写会千变万化,但是其也有处理标准,比如:针对运行时注解会采用反射机制处理, 针对编译时注解会采用 AbstractProcessor 来处理。本节就针对前面讲到的运行时注解和编译时注解来编写注解处理器。

运行时的注解处理器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class AnnonationTest {
    @Test
    String defaultName;
    @Test(name = "111111")
    String name;

    public static void main(String[] args) {
        Field[] fields = AnnonationTest.class.getDeclaredFields();
        for (Field field : fields) {
            Test test = field.getAnnotation(Test.class);
            System.out.println(test.name());
        }
    }
}
//运行结果
郭翰林
111111

编译时的注解处理器

由于反射机制会有性能消耗,如果应用程序频繁的进行反射则会导致应用卡顿。所以类似于ButterknifeDagger的依赖注入框架都采用了编译时的注解处理器。

编译时的注解处理器,主要通过继承AbstractProcessor 实现了process方法,来生成想要的逻辑代码(.java文件),触发编译时静态的存储在工程目录本地。

 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
49
50
51
52
53
54
55
56
57
58
59
60
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    private Messager messager;
    private Elements elements;

    /**
     * 注释:可以得到ProcessingEnviroment,
       ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
     * 时间:2021/2/18 0018 15:39
     * 作者:郭翰林
     *
     * @param processingEnv
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        elements = processingEnv.getElementUtils();
    }

    /**
     * 注释:指定这个注解处理器是注册给哪个注解的
     * 时间:2021/2/18 0018 15:39
     * 作者:郭翰林
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(BindView.class.getCanonicalName());
        return supportTypes;
    }

    /**
     * 注释:指定使用的Java版本
     * 时间:2021/2/18 0018 15:39
     * 作者:郭翰林
     *
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 注释:具体的处理过程
     * 时间:2021/2/18 0018 15:40
     * 作者:郭翰林
     * @param annotations
     * @param roundEnv
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        .....
        return false;
    }
}

PS:尽管Butterknife最后还是采用了反射,但是由于他使用了一个LinkHashMap记录了反射对象,理论上一个类里的注解只会被反射一次,所以性能消耗有限。

五、Android中的APT技术的基本使用

我们来模仿Butterknife的@BindView注解来阐述,Android业务场景中我们应该怎么去自定义我们的注解,以及配合APT技术来生成我们想要的模板代码,来简化我们日常开发中的逻辑。

第一步:创建自定义注解以及注解处理器Java模块

创建模块目录如下图所示,添加自定义注解以及自定义注解处理器。这里需要注意的是创建时一定需要创建JAVA模块,要不然后续无法通过annotationProcessor引用模块。

注解处理器模块的Gradle脚本引用如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(":apt-annotation")
    implementation 'com.squareup:javapoet:1.10.0'
    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor  'com.google.auto.service:auto-service:1.0-rc6'
}

sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8

这里我们可以看到这边使用了2个三方库

库名用途
com.squareup:javapoet辅助生成JAVA代码的三方库
com.google.auto.service:auto-service辅助生成META-INF/services/javax.annotation.processing.Processor文件

规定Java注解处理器模块,必须的在main 目录下新建 resources 资源文件夹,并 resources 中再建立META-INF/services目录 文件夹。最后在META-INF/services中创建 javax.annotation.processing.Processor文件,这个文件中的内容是 注解处理器的名称。为了省去麻烦的创建过程,我们使用了Google提供的AutoService库来帮助我们动态生成,下图可以看到动态生成的截图。

第二步:书写自定义注解以及自定义注解处理器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/**
 * @author 郭翰林
 * @date 2021/2/18 0018 14:23
 * 注释:定义绑定视图注解
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    @IdRes int value();
}
 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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    private Messager mMessager;
    private Elements mElements;
    private Map<String, ClassCreatorProxy> mProxyMap = new HashMap<>();

    /**
     * 注释:可以得到ProcessingEnviroment,ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
     * 时间:2021/2/18 0018 15:39
     * 作者:郭翰林
     *
     * @param processingEnv
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mMessager = processingEnv.getMessager();
        mElements = processingEnv.getElementUtils();
    }

    /**
     * 注释:指定这个注解处理器是注册给哪个注解的
     * 时间:2021/2/18 0018 15:39
     * 作者:郭翰林
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(BindView.class.getCanonicalName());
        return supportTypes;
    }

    /**
     * 注释:指定使用的Java版本
     * 时间:2021/2/18 0018 15:39
     * 作者:郭翰林
     *
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 注释:具体的处理过程
     * 时间:2021/2/18 0018 15:40
     * 作者:郭翰林
     *
     * @param annotations
     * @param roundEnv
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, "BindViewProcessor开始编译...");
        mProxyMap.clear();
        //得到所有的注解
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
        for (Element element : elements) {
            VariableElement variableElement = (VariableElement) element;
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            String fullClassName = classElement.getQualifiedName().toString();
            ClassCreatorProxy proxy = mProxyMap.get(fullClassName);
            if (proxy == null) {
                proxy = new ClassCreatorProxy(mElements, classElement);
                mProxyMap.put(fullClassName, proxy);
            }
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
            int id = bindAnnotation.value();
            proxy.putElement(id, variableElement);
        }
        //通过javapoet生成
        for (String key : mProxyMap.keySet()) {
            ClassCreatorProxy proxyInfo = mProxyMap.get(key);
            JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(), proxyInfo.generateJavaCode()).build();
            try {
                // 生成文件
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        mMessager.printMessage(Diagnostic.Kind.NOTE, "BindViewProcessor编译结束...");
        return true;
    }
}

第三步:创建反射调用模块

在前面2步,完成之后其实,APT注解工作已经完成了,此时把注解运用到主模块代码中,编译即可看到APT自动生成的代码

但是生成的代码我们肯定是要用来在代码中调用的,所以需要创建一个反射调用模块来调用APT动态生成的代码,但是注意此时创建的是一个Android Lib而不是Java模块。

 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
public class BindViewTool {
    /**
     * 注释:通过反射调用Apt生成的代码以执行绑定视图逻辑
     * 时间:2021/2/18 0018 17:51
     * 作者:郭翰林
     *
     * @param activity
     */
    public static void bind(Activity activity) {
        Class clazz = activity.getClass();
        try {
            Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
            Method method = bindViewClass.getMethod("bind", activity.getClass());
            method.invoke(bindViewClass.newInstance(), activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    api project(":apt-annotation")
    ....
}

从上面的依赖关系可以看到,这里把自定义注解模块也给引用了进去,主要是为了在主模块中引用时不用重复引用。

第四步:主模块中引用并验证

首先添加Gradle依赖

1
2
3
4
5
6
7
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    ....
    implementation project(":apt-library")
    annotationProcessor project(":apt-processor")
    ...
}

然后在需要使用的地方引用自定义注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MainActivity extends AppCompatActivity {
    @BindView(value = R.id.toolbar)
    Toolbar toolbar;

    @BindView(value = R.id.fab)
    FloatingActionButton fab;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setSupportActionBar(toolbar);
        BindViewTool.bind(this);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });
    }
}

运行代码,可以看到自定义注解处理器生成的日志,以及生成的APT代码截图

Demo:https://github.com/RmondJone/Android-APT

我的个人博客:https://rmondjone.github.io/

Built with Hugo
主题 StackJimmy 设计