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的源码,其中可以看到很多有意义的实现。
推荐阅读
-
Android 编译时注解
-
iOS架构-静态库.a编译时自动导出.h头文件(24)
-
android 注解简介三: 自定义注解实现视图绑定
-
微信浏览器弹出框滑动时页面跟着滑动的实现代码(兼容Android和IOS端)
-
Android 解决dialog弹出时无法捕捉Activity的back事件问题
-
Android应用程序的编译流程及使用Ant编译项目的攻略
-
Android应用中图片浏览时实现自动切换功能的方法详解
-
为Android的apk应用程序文件加壳以防止反编译的教程
-
Android Studio gradle 编译提示‘default not found’ 解决办法
-
linux下vscode编译和调试时链接库