Android 编译时注解

这两天浏览博客,看到关于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中可能需要添加一些配置参数,如图:

这里写图片描述

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

2. 定义注解处理器

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

这里写图片描述

在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的方法,就被看做为要被生成的接口。具体我就不做解释了。
注意:在测试时发现,可能会在该文件中使用中文注释会报如下的错误:

这里写图片描述

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

这里写图片描述

这样就可以了。

3. 注解处理器运行

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

这里写图片描述

之后,我们在主工程中创建测试类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可以复制类的全限定类名),如下:

这里写图片描述

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

这里写图片描述

这里写图片描述

4.AutoService

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

这里写图片描述

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

这里写图片描述

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

5. android-apt

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

这里写图片描述

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

这里写图片描述

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

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

项目git地址

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值