简介

在android开发中,比较常用到的第三方库中,有不少用到了 注解处理器(Annotation Processor)
比较常见的就有 ButterknifeDagger2DBFlow 等。

注解

Java中存在不少关于注解的Api, 比如@Override用于覆盖父类方法,@Deprecated表示已舍弃的类或方法属性等,android中又多了一些注解的扩展,如@NonNull, @StringRes, @IntRes等。

代码自动生成

使用代码自动生成,一是为了提高编码的效率,二是避免在运行期大量使用反射,通过在编译期利用反射生成辅助类和方法以供运行时使用。

注解处理器的处理步骤主要有以下:
1. 在java编译器中构建
2. 编译器开始执行未执行过的注解处理器
3. 循环处理注解元素(Element),找到被该注解所修饰的类,方法,或者属性
4. 生成对应的类,并写入文件
5. 判断是否所有的注解处理器都已执行完毕,如果没有,继续下一个注解处理器的执行(回到步骤1)

Butterknife注解处理器的例子

Butterknife的注解处理器的工作方式如下:
1. 定义一个非私有的属性变量
2. 添加该属性变量的注解和id
3. 调用Butterknife.bind(..)方法。

当你点击Android Studio的Build按钮时,Butterknife先是按照上述步骤生成了对应的辅助类和方法。在代码执行到bind()方法时,Butterknife就去调用之前生成的辅助类方法,,完成对被注解元素的赋值操作。


自定义注解处理器

了解了基本的知识点后,我们应该尝试去使用这些技巧。
接下来是实践时间,我们来开发一个简单的例子,利用注解处理器来自动产生随机数字和随机字符串。
1. 首先创建一个project。
2. 创建lib_annotations,这是一个纯java的module,不包含任何android代码,只用于存放注解。
3. 创建lib_compiler,这同样是一个纯java的module。该module依赖于步骤2创建的module_annotation,处理注解的代码都在这里,该moduule最终不会被打包进apk,所以你可以在这里导入任何你想要的任意大小依赖库。
4. 创建lib_api,对该module不做要求,可以是android library或者java library或者其他的。该module用于调用步骤3生成的辅助类方法。

1.添加注解

在lib_annotations中添加两个注解:RandomString, RandomInt,分别用于生成随机数字和随机字符串:

@Retention(CLASS)@Target(value = FIELD)public @interface RandomString {}
@Retention(CLASS)@Target(value = FIELD)public @interface RandomInt {    int minValue() default 0;    int maxValue() default 65535;}
  • @interface
    自定义注解,使用 @interface 作为类名修饰符
  • @Target
    该注解所能修饰的元素类型,可选类型如下:
public enum ElementType {    TYPE, //类    FIELD, //属性    METHOD, //方法    PARAMETER, //参数    CONSTRUCTOR, //构造函数    LOCAL_VARIABLE,     ANNOTATION_TYPE,    PACKAGE,    TYPE_PARAMETER,    TYPE_USE;    private ElementType() {    }}
  • @Retention
    该注解的保留策略,有三种选项:
public enum RetentionPolicy {    SOURCE, //被编译器所忽略    CLASS, //被编译器保留至类文件,但不会保留至运行时    RUNTIME //保留至类文件,且保留至运行时,能在运行时反射该注解修饰的对象}

2. 注解处理器

真正处理注解并生成代码的操作都在这里。
在写代码之前我们需要先导入两个重要的库,以及我们的注解模块:

compile 'com.google.auto.service:auto-service:1.0-rc4'compile 'com.squareup:javapoet:1.9.0'implementation project(':lib_annotations')

新建类RandomProcessor.java:

@AutoService(Processor.class)public class RandomProcessor extends AbstractProcessor{    @Override    public synchronized void init(ProcessingEnvironment processingEnvironment) {        super.init(processingEnvironment);    }    @Override    public SourceVersion getSupportedSourceVersion() {        return super.getSupportedSourceVersion();    }    @Override    public Set getSupportedAnnotationTypes() {        return super.getSupportedAnnotationTypes();    }    @Override    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {        return false;    }}
  • @AutoService
    @AutoService(Processor.class)会告诉编译器该注解处理器的存在,并在编译时自动在META-INF/services下生成javax.annotation.processing.Processor文件,文件的内容为
com.rhythm7.lib_compiler.RandomProcessor

也就是说,你所声明的注解处理器都会在被写入这个配置文件中。
这样子,当外部程序装载这个模块的时候,就能通过该模块的jar包下的META-INF/services下找到具体的注解处理器的实现类名,并加载实例化,完成模块的注入。
注解处理器需要实现AbstractProcessor接口,并实现对应的方法
- init() 可选
在该方法中可以获取到processingEnvironment对象,借由该对象可以获取到生成代码的文件对象, debug输出对象,以及一些相关工具类

  • getSupportedSourceVersion()
    返回所支持的java版本,一般返回当前所支持的最新java版本即可

  • getSupportedAnnotationTypes()
    你所需要处理的所有注解,该方法的返回值会被process()方法所接收

  • process() 必须实现
    扫描所有被注解的元素,并作处理,最后生成文件。该方法的返回值为boolean类型,若返回true,则代表本次处理的注解已经都被处理,不希望下一个注解处理器继续处理,否则下一个注解处理器会继续处理。

初始化

较详细代码如下:

private static final List> RANDOM_TYPES        = Arrays.asList(RandomInt.class, RandomString.class);private Messager messager;private Types typesUtil;private Elements elementsUtil;private Filer filer;private TypeonProcess()per.init(processingEnv);    messager = processingEnv.getMessager();     typesUtil = processingEnv.getTypeUtils();    elementsUtil = processingEnv.getElementUtils();    filer = processingEnv.getFiler();}@Overridepublic SourceVersion getSupportedSourceVersion() {    return SourceVersion.latestSupported();}@Overridepublic Set getSupportedAnnotationTypes() {    Set annotations = new LinkedHashSet<>();    for (Class<? extends Annotation> annotation : RANDOM_TYPES) {        annotations.add(annotation.getCanonicalName());    }    return annotations;}

处理注解

process()方法中执行以下操作:
1. 扫描所有注解元素,并对注解元素的类型做判断

for (Element element : roundEnv.getElementsAnnotatedWith(RandomInt.class)) {    //AnnotatedRandomInt是对被RandomInt注解的Elment的简单封装    AnnotatedRandomInt randomElement = new AnnotatedRandomInt(element);    messager.printMessage(Diagnostic.Kind.NOTE, randomElement.toString());    //判断被注解的类型是否符合要求    if (!element.asType().getKind().equals(TypeKind.INT)) {         messager.printMessage(Diagnostic.Kind.ERROR, randomElement.getSimpleClassName().toString() + "#"          + randomElement.getElementName().toString() + " is not in valid type int");    }    //按被注解元素所在类的完整类名为key将被注解元素存储进Map中,后面会根据key生成类文件    String qualifier = randomElement.getQualifiedClassName().toString();    if (annotatedElementMap.get(qualifier) == null) {        annotatedElementMap.put(qualifier, new ArrayList());    }    annotatedElementMap.get(qualifier).add(randomElement);}

生成类文件

将之前以注解所在类为key的map遍历,并以key值为分组生成类文件。

for (Map.Entry entry : annotatedElementMap.entrySet()) {    MethodSpec constructor = createConstructor(entry.getValue());    TypeSpec binder = createClass(getClassName(entry.getKey()), constructor);    JavaFile javaFile = JavaFile.builder(getPackage(entry.getKey()), binder).build();    javaFile.writeTo(filer);}

生成类、构造函数、代码段以及文件都是利用到了javapoet依赖库。当然你也可以选择拼接字符串和自己用文件IO写入,但是用javapoet要更方便得多。

private MethodSpec createConstructor(List randomElements) {    AnnotatedRandomElement firstElement = randomElements.get(0);    MethodSpec.Builder builder = MethodSpec.constructorBuilder()            .addModifiers(Modifier.PUBLIC)            .addParameter(TypeName.get(firstElement.getElement().getEnclosingElement().asType()), "target");    for (int i = 0; i < randomElements.size(); i++) {        addStatement(builder, randomElements.get(i));    }    return builder.build();}private void addStatement(MethodSpec.Builder builder, AnnotatedRandomElement randomElement) {    builder.addStatement(String.format(            "target.%1$s = %2$s",            randomElement.getElementName().toString(),            randomElement.getRandomValue())    );}private TypeSpec createClass(String className, MethodSpec constructor) {    return TypeSpec.classBuilder(className + "_Random")            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)            .addMethod(constructor)            .build();}private String getPackage(String qualifier) {    return qualifier.substring(0, qualifier.lastIndexOf("."));}private String getClassName(String qualifier) {    return qualifier.substring(qualifier.lastIndexOf(".") + 1);}

通过以上几行代码,创建了类文件。在类的构造函数中添加参数(target), 并为每一个被注解元素添加语句”target.%1 s= s = s”,最后通过javaFile.writeTo(filer)完成文件写入。

3. 调用生成类的方法

在lib_api中新建一个类:RandomUtil.java,添加注入方法:

public static void inject(Object object) {    Class bindingClass = Class.forName(object.getClass().getCanonicalName() + "_Random");     Constructor constructor = bindingClass.getConstructor(object.getClass());    constructor.newInstance(object);}

这里利用反射找到了以“Object类名_Random”命名的生成类,并调用它的构造方法。而在我们之前的注解处理器中,我们已在生成类的构造方法中实现了属性的赋值操作。

4. 使用生成类

在app module中依赖刚才创建的库:

implementation project(':lib_annotations')implementation project(':lib_api')annotationProcessor project(':lib_compiler')

在Activity中的使用

public class MainActivity extends AppCompatActivity {    @RandomInt(minValue = 10, maxValue = 1000)    int mRandomInt;    @RandomString    String mRandomString;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        RandomUtil.inject(this);        Log.i("RandomInt ==> ", mRandomInt + "");        Log.i("RandomString ==> ", mRandomString);    }}

编译,运行,查看结果:

02-11 18:38:51.747 7887-7887/com.rhythm7.annotationprocessordemo I/RandomInt ==>: 70002-11 18:38:51.747 7887-7887/com.rhythm7.annotationprocessordemo I/RandomString ==>: HhRayFyTtt

被注解的元素成功被自动赋值,说明注入成功。

完整的demo地址:github

原文地址:http://www.unfishable.com


调试

注解处理器的debug跟普通的代码debug有点不同:

在当前工程路径下输入命令

gradlew --no-daemon -Dorg.gradle.debug=true :app:clean :app:compileDebugJavaWithJavac

并在Edit Configurations中新添加一个远程配置(remote),名字随意,端口为5005。
然后点击debug按钮,就可以连接上远程调试器进行Annotation的调试了。

更多相关文章

  1. android 一步一步创建jni示例
  2. android 创建简单通用工程
  3. Android应用程序启动画面
  4. Use Android(安卓)NDK r7 to compile FFmpeg0.8.1 version for A
  5. Android(安卓)代码生成界面Sample
  6. Android的配置文件操作的完美封装(使用注解 反射让配置文件操作如
  7. android工程gen目录无法生成的解决办法汇总
  8. Android(安卓)Studio创建Android项目生成的文件详解
  9. Flutter 学习之打包 - 纯Flutter项目生成Android包

随机推荐

  1. android之有返回结果跳转intent
  2. 通过终端命令生成并在手机上运行dex文件
  3. Robotium---环境搭建及入门示例
  4. Android学习笔记:Android基础知识总结
  5. android的技术层次
  6. Spark实例-每天每个搜索词用户访问
  7. Android(安卓)JNI(实现自己的JNI_OnLoad函
  8. Edittext 各种属性
  9. Android之back键拦截处理
  10. 【Android】android的基本UI操作(1)