欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android 编译时注解

程序员文章站 2024-02-29 23:22:34
...

这两天浏览博客,看到关于ButterKnife的源码解析,提到了编译时注解这个技术点,貌似还没玩过,就跳着翻看了一下《Think in Java》的注解章节,作者是基于Java5来进行注解相关的讲解,其中提供了Mirror API用于处理被注解元素,以及apt工具进行编译期注解处理。但在jdk1.8中已经将apt工具抛弃(apt工具是oracle提供的私有实现,在JDK开发包的类库中是不存在的),且使用新的api(java.annotation.processing(注解处理器API)与javax.lang.model(注解元素API))。于是就到网上搜寻一翻,经过两天的研究,终于把编译时注解开发流程摸清,在此记录。

一、概述

具体的注解相关概念就不赘述了。说一说这个编译时注解,简单来说,就是在编译期间调用注解处理器对注解进行处理生成新源文件,后再检查新源文件中的注解再处理,直到没有新的源文件产生,就开始编译所有的源文件。
本文不以ButterKnife为例做讲解,防止增加复杂性,以《Think in Java》中的例子:提取类中的public方法生成接口。本文重点在于AndroidStudio上进行编译时注解的实现。

二、实现

1.新建项目,并创建Module:annotations

这个module必须要是Java Library,用于保存注解。这个要和下面的注解处理器processor分开。解释一下原因,因为后面我们在打包时不需要将processor打包进去,要单独去掉。
新建注解类 InterfaceExtractor.java。

package com.example;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 接口提取注解
 * Created by DavidChen on 2017/7/20.
 */

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface InterfaceExtractor {
    String value();
}

注意:网上看到部分博客说在annotations的build.gradle中可能需要添加一些配置参数,如图:

Android 编译时注解

但是,我新建的时候直接就有了,也没测试到底有没有影响。如果大家发现有影响,请自行添加。

2. 定义注解处理器

新建Moudle:processor,这里也要是Java Library的module,因为要这里使用到javax包,在Android中没有javax.annotation相关资源。之后在processor的build.gradle中添加对annotations的module的依赖,如图:

Android 编译时注解

在processor新建InterfaceExtractorProcessor.java类,该类继承自javax.annotation.processing.AbstractProcessor,在编译期间,编译器会自动调用该类进行注解处理。

package com.example;

import java.io.IOException;
import java.io.Writer;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Elements;
import javax.tools.JavaFileObject;

public class InterfaceExtractorProcessor extends AbstractProcessor {

    private Filer mFiler;   // 注解处理器创建文件的File工具
    private Elements mElementUtils; // 相关元素处理工具

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotationsType = new LinkedHashSet<>();
        annotationsType.add(InterfaceExtractor.class.getCanonicalName());
        return annotationsType;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_7;
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 获取所有使用该注解的元素
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(InterfaceExtractor.class);
        for (Element element : elements) {
            TypeElement typeElement = (TypeElement) element;
            PackageElement packageElement = mElementUtils.getPackageOf(typeElement);
            InterfaceExtractor annotation = typeElement.getAnnotation(InterfaceExtractor.class);
            String packageName = packageElement.getQualifiedName().toString();
            String className = annotation.value();
            JavaFileObject sourceFile;
            try {
                sourceFile = mFiler.createSourceFile(packageName + "." + className, typeElement);
                Writer writer = sourceFile.openWriter();
                writer.write(generateJavaCode(packageName, className, typeElement));
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    private String generateJavaCode(String packageName, String className, TypeElement typeElement) {
        StringBuilder builder = new StringBuilder();
        builder.append("// Generated code. Do not modify!\n");
        builder.append("package ").append(packageName).append(";\n\n");
        builder.append('\n');
        builder.append("public interface ").append(className).append(" {\n\n");
        List<? extends Element> executableElements = typeElement.getEnclosedElements();
        for (Element element : executableElements) {
            if (element instanceof ExecutableElement) {
                ExecutableElement executableElement = (ExecutableElement) element;
                if (executableElement.getModifiers().contains(Modifier.PUBLIC) &&
                        !(executableElement.getModifiers().contains(Modifier.STATIC)) &&
                        executableElement.getKind() != ElementKind.CONSTRUCTOR) {
                    builder.append("public ");
                    builder.append(executableElement.getReturnType());
                    builder.append(" ").append(executableElement.getSimpleName()).append(" (");
                    List<? extends VariableElement> variableElements = executableElement.getParameters();
                    int i = 0;
                    for (VariableElement variableElement : variableElements) {
                        builder.append(variableElement.asType()).append(" ").append(variableElement.getSimpleName());
                        if (++i < variableElements.size()) {
                            builder.append(", ");
                        }
                    }
                    builder.append(");\n\n");
                }
            }
        }
        builder.append("}\n");
        return builder.toString();
    }
}

先看看我们实现了哪些方法:
process()是处理注解的核心方法;
1. init()用于根据ProcessingEnvironment来出初始化处理环境,比如提供各种处理工具;
2. getSupportedAnnotationTypes()则用于指定处理的注解类型;
3. getSupportedSourceVersion()则指定使用的java版本,其中如果不指定则返回javaSE6。这里我使用的是JavaSE7。有博客上使用下面这两个注解来代替上述的两个方法,但是也有说着两个注解对Android兼容性不太好。没做测试,就不发表言论了。

  • @SupportedSourceVersion(SourceVersion.RELEASE_7)
  • @SupportedAnnotationTypes(“com.example.InterfaceExtractor”)

再说一下这里的process方法,就是以注解的参数为接口名,遍历被注解类下所有的方法,只要是public且不是构造或static的方法,就被看做为要被生成的接口。具体我就不做解释了。
注意:在测试时发现,可能会在该文件中使用中文注释会报如下的错误:

Android 编译时注解

解决办法是在processor的build.gradle下添加如下task:

Android 编译时注解

这样就可以了。

3. 注解处理器运行

写完上述的两个module,我们需要在主工程app中添加两个module的依赖,如图:

Android 编译时注解

之后,我们在主工程中创建测试类Test.java

package com.example.davidchen.annotationsample;

import com.example.InterfaceExtractor;

/**
 * 测试注解
 * Created by DavidChen on 2017/7/20.
 */

@InterfaceExtractor("ITest")
public class Test {

    public Test(String s1) {

    }

    public String hello(String s2) {
        return s2;
    }

    public void say() {

    }

    public int says(int s3, float a) {
        return (int) (s3 + a);
    }

    public static String s1() {
        return null;
    }

    private String s2() {
        return null;
    }

}

这里就随便写了几个方法,用于测试。注解里传入“ITest”作为生成的接口名称。现在如果我们make project,我们在…\app\build\generated\source\apt\debug目录下是看不到任何的输出的。因为,还少了对processor的注册。

首先在processor下main目录下新建resources目录,再建立META-INF目录,再建立services目录,之后新建javax.annotation.processing.Processor文件,并把我们自定义的InterfaceExtractorProcessor的全限定类名复制到该文件内(有个小技巧,右击类,选择Copy Reference可以复制类的全限定类名),如下:

Android 编译时注解

这样,我们clean Project,再Make Project就可以在…\app\build\generated\source\apt\debug\目录生成我们需要的java文件了。同时也可以在…\app\build\intermediates\classes\debug\中看到我们生成的java文件编译后的class文件。如下:

Android 编译时注解

Android 编译时注解

4.AutoService

在我们注册processor时,有木有发现很繁琐?是的,可能就建立几个文件夹和文件,复制类名粘贴而已,但是我们需要更简单的方法。那就是Auto-Service,可以帮我们自动生成所需要的processor相关配置。
首先在processor的build.gradle中添加依赖:

Android 编译时注解

再在InterfaceExtractor类中添加注解即可。

Android 编译时注解

这时我们再clean、make project,就可以获得同样的效果,省去了繁琐的操作。

5. android-apt

啥是android-apt呢,其实就是个插件。干嘛用的呢?帮助我们在编译时依赖处理器,而在生成apk时就把它去掉不包含在包中,这样就省得把一些无用的东西打包到apk中了。
首先配置项目的build.gradle,如下:

Android 编译时注解

然后是主工程的build.gradle,如下:

Android 编译时注解

ok了,其他啥也不用干,我测试了一下,之前没用apt插件打包是2238KB,使用之后是1456KB。

好了,到这里就差不多了,这里的例子只是简单的使用编译注解,没多大意义,大家可以看看ButterKnife的源码,其中可以看到很多有意义的实现。

项目git地址

参考:
自定义注解之编译时注解(RetentionPolicy.CLASS)(一)

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

Android开发之手把手教你写ButterKnife框架(二)