暴力突破 Android 编译插桩(二)- APT 之注解知识

专栏暴力突破 Android 编译插桩系列

一、前言


不管是在 Java 后端开发还是 Android 开发,Java 注解都有很广泛的运用。在 Android 中要想看懂很多开源库如 ARouter、dagger、ButterKnife 等都不得不弄懂注解知识,想更好地提升开发效率和代码质量注解可以帮上很大的忙。下面我们一起来学习注解知识。

 

二、注解定义


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

注解不能运行,它只有成员变量,没有方法,跟 public、final 等修饰符的地位一样,都是程序元素的一部分,不能作为一个程序元素使用。其实大家都是用过注解的,只是可能没有过深入了解它的原理和作用,比如肯定见过 @Override 、@Deprecated 等。注解可以将一些本来重复性的工作变成程序自动完成,简化和自动化该过程。比如用于生成 Java doc、编译时进行格式检查、自动生成代码等,用于提升软件的质量和提高软件的生产效率。

 

三、元注解


平时我们使用的注解有来自 JDK 里包含的,也有 Android SDK 里包含的,也可以自定义。这些注解是怎么创建的呢?Java 提供了四种元注解来专门负责新注解的创建工作,即注解其他注解

3.1、@Target

定义了 Annotation 所修饰的对象范围,取值: 

  • ElementType.CONSTRUCTOR:用于描述构造器
  • ElementType.FIELD:用于描述域
  • ElementType.LOCAL_VARIABLE:用于描述局部变量
  • ElementType.METHOD:用于描述方法
  • ElementType.PACKAGE:用于描述包
  • ElementType.PARAMETER:用于描述参数
  • ElementType.TYPE:用于描述类、接口(包括注解类型)或 enum 声明

3.2、@Retention

定义了该 Annotation 被保留的时间长短,取值:

  • RetentionPoicy.SOURCE:注解只保留在源文件,当 Java 文件编译成 class 文件的时候注解被遗弃。可以用于做一些检查性的操作,比如 @Override 和 @SuppressWarnings
  • RetentionPoicy.CLASS:注解被保留到 class 文件,但 jvm 加载 class 文件时被遗弃,这是默认的生命周期。可以用于在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife)
  • RetentionPoicy.RUNTIME:注解不仅被保存到 class 文件中,jvm 加载 class 文件之后仍然存在。可以用于在运行时去动态获取(反射)注解信息。

3.3、@Documented

标记注解,用于描述其它类型的注解应该被作为被标注的程序成员的公共 API,因此可以被例如 javadoc 类的工具文档化,不用赋值。

3.4、@Inherited

标记注解,允许子类继承父类的注解。 这里一开始有点理解不了,需要断句一下,允许子类继承父类的注解。示例:

@Target(value = ElementType.TYPE)  
@Retention(RetentionPolicy.RUNTIME)  
@Inherited  
public @interface Sample {   
    public String name() default "";      
}

@Sample  
class Test{  
}  

class Test2 extents Test{  
}  

这样类 Test2 其实也有注解 @sample 。

另外在写法上,如果成员名称是 value,则在赋值过程中可以简写。如果成员类型为数组但是只赋值一个元素,则也可以简写。示例以下三个写法都是等价的。

正常写法:

@Documented
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.ANNOTATION_TYPE})
public @interface Target {
    ElementType[] value();
}

省略 value 的写法(只有成员名称是 value 时才能省略):

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Target {
    ElementType[] value();
}

成员类型是数组,只赋值一个元素的简写:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

 

四、其他注解


4.1、JDK 内置的其他注解 

@Override、@Deprecated、@SuppressWarnings、@SafeVarargs、@FunctionalInterface、@Resources等。

4.2、Android SDK 内置的注解 

Android SDK 内置的注解都在 com.android.support:support-annotations 包里,下面以 'com.android.support:support-annotations:25.2.0' 为例

  • 资源引用限制类:用于限制参数必须为对应的资源类型

@AnimRes、@AnyRes、@ArrayRes、@AttrRes、@BoolRes、@ColorRes等。

  • 线程执行限制类:用于限制方法或者类必须在指定的线程执行

@AnyThread、@BinderThread、@MainThread、@UiThread、@WorkerThread。

  • 参数为空性限制类:用于限制参数是否可以为空

@NonNull、@Nullable。

  • 类型范围限制类:用于限制标注值的值范围

@FloatRang、@IntRange。

  • 类型定义类:用于限制定义的注解的取值集合

@IntDef、@StringDef。

  • 其他的功能性注解:

@CallSuper、@CheckResult、@ColorInt、@Dimension、@Keep、@Px、@RequiresApi、@RequiresPermission、@RestrictTo、@Size、@VisibleForTesting。

 

五、自定义注解


使用收益最大的,还是需要根据自身需求自定义注解,下面依次介绍三种类型的注解自定义示例:

5.1、RetentionPolicy.SOURCE

一般函数的参数值有限定的情况,比如 View.setVisibility 的参数就有限定,可以看到 View.class 源码里除了 @IntDef,还有 @StringDef。

@IntDef({VISIBLE, INVISIBLE, GONE})
@Retention(RetentionPolicy.SOURCE)
public @interface Visibility {}

public static final int VISIBLE = 0x00000000;
public static final int INVISIBLE = 0x00000004;
public static final int GONE = 0x00000008;
    
public void setVisibility(@Visibility int visibility) {
    setFlags(visibility, VISIBILITY_MASK);
}

5.2、RetentionPolicy.RUNTIME

运行时注解的定义如下:

// 适用类、接口(包括注解类型)或枚举  
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.TYPE)  
public @interface ClassInfo {  
    String value();  
}  
// 适用field属性,也包括enum常量  
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.FIELD)  
public @interface FieldInfo {  
    int[] value();  
}  
// 适用方法  
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.METHOD)  
public @interface MethodInfo {  
    String name() default "long";  
    int age() default 27;  
}  

定义一个测试类来使用这些注解:

@ClassInfo("Test Class")  
public class TestRuntimeAnnotation {  
  
    @FieldInfo(value = {1, 2})  
    public String fieldInfo = "FiledInfo";  
    
    @MethodInfo(name = "BlueBird")  
    public static String getMethodInfo() {  
        return return fieldInfo;  
    }  
}  

使用注解:

private void _testRuntimeAnnotation() {  
    StringBuffer sb = new StringBuffer();  
    Class<?> cls = TestRuntimeAnnotation.class;  
    Constructor<?>[] constructors = cls.getConstructors();  
    // 获取指定类型的注解  
    sb.append("Class注解:").append("\n");  
    ClassInfo classInfo = cls.getAnnotation(ClassInfo.class);  
    if (classInfo != null) {  
        sb.append(cls.getSimpleName()).append("\n");  
        sb.append("注解值: ").append(classInfo.value()).append("\n\n");  
    }  
  
    sb.append("Field注解:").append("\n");  
    Field[] fields = cls.getDeclaredFields();  
    for (Field field : fields) {  
        FieldInfo fieldInfo = field.getAnnotation(FieldInfo.class);  
        if (fieldInfo != null) {  
            sb.append(field.getName()).append("\n");  
            sb.append("注解值: ").append(Arrays.toString(fieldInfo.value())).append("\n\n");  
        }  
    }  
  
    sb.append("Method注解:").append("\n");  
    Method[] methods = cls.getDeclaredMethods();  
    for (Method method : methods) {  
        MethodInfo methodInfo = method.getAnnotation(MethodInfo.class);  
        if (methodInfo != null) {  
            sb.append(Modifier.toString(method.getModifiers())).append(" ")  
                    .append(method.getName()).append("\n");  
            sb.append("注解值: ").append("\n");  
            sb.append("name: ").append(methodInfo.name()).append("\n");  
            sb.append("age: ").append(methodInfo.age()).append("\n");  
        }  
    }  
  
    System.out.print(sb.toString());  
}  

所做的操作都是通过反射获取对应元素,再获取元素上面的注解,最后得到注解的属性值。因为涉及到反射,所以运行时注解的效率多少会受到影响,现在很多的开源项目使用的是编译时注解。

5.3、RetentionPolicy.CLASS

步骤如下:

5.3.1 添加依赖(根据情况)

如果 Gradle 插件版本是 2.2 以上的话,不需要添加以下android-apt依赖。

classpath 'com.android.tools.build:gradle:2.2.1'

在整个工程的 build.gradle 中添加 android-apt 的依赖:

buildscript {  
    repositories {  
        jcenter()  
        mavenCentral()  // add  
    }  
    dependencies {  
        classpath 'com.android.tools.build:gradle:2.1.2' //2.2以上无需添加apt依赖 
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'  // add  
    }  
} 

 android-apt 是一个 Gradle 插件,协助 Android Studio 处理 annotation processors,它有两个目的:

  • 允许配置只在编译时作为注解处理器的依赖,而不添加到最后的 APK 或 library
  • 设置源路径,使注解处理器生成的代码能被 Android Studio 正确的引用

伴随着 Android Gradle 插件 2.2 版本的发布,android-apt 作者在官网发表声明证实了后续将不会继续维护 android-apt,并推荐大家使用 Android 官方插件提供的相同能力。也就是说,大约五年前推出的 android-apt 即将告别开发者,退出历史舞台,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt。

5.3.2 定义要使用的注解

建一个 Java 库来专门放注解,示例库名为 annotations。定义注解:

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

5.3.3 定义注解处理器

另外建一个 Java 库工程,示例库名为 processors,这里必须为 Java 库,不然会找不到 javax 包下的相关资源。processors.build.gradle 要依赖以下内容:

compile 'com.google.auto.service:auto-service:1.0-rc2'
compile 'com.squareup:javapoet:1.9.0'    
compile(project(':annotations'))

其中:

  • auto-service 用于自动在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
  • javapoet 用于产生 .java 源文件的辅助库,它可以很方便地帮助我们生成需要的.java 源文件。(关于 javapoet 的请看我的 Android javapoet 使用解析

示例代码如下:

package com.example;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

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

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
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.Modifier;
import javax.lang.model.element.TypeElement;

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {

    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        // Filer是个接口,支持通过注解处理器创建新文件
        filer = processingEnv.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement element : annotations) {
            //新建文件
            if (element.getQualifiedName().toString().equals(MyAnnotation.class.getCanonicalName())) {
                // 创建main方法
                MethodSpec main = MethodSpec.methodBuilder("main")
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                        .returns(void.class)
                        .addParameter(String[].class, "args")
                        .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
                        .build();
                // 创建HelloWorld类
                TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                        .addMethod(main)
                        .build();

                try {
                    // 生成 com.example.HelloWorld.java
                    JavaFile javaFile = JavaFile.builder("com.example", helloWorld)
                            .addFileComment(" This codes are generated automatically. Do not modify!")
                            .build();
                    // 生成文件
                    javaFile.writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        //在Gradle console 打印日志
        for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
            System.out.println("------------------------------");
            // 判断元素的类型为Class
            if (element.getKind() == ElementKind.CLASS) {
                // 显示转换元素类型
                TypeElement typeElement = (TypeElement) element;
                // 输出元素名称
                System.out.println(typeElement.getSimpleName());
                // 输出注解属性值System.out.println(typeElement.getAnnotation(MyAnnotation.class).value());
            }
            System.out.println("------------------------------");
        }
        return true;
    }

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

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

5.3.4 在代码中使用定义的注解 

需要依赖上面的两个 java 库 annotations 和 processors。

import com.example.MyAnnotation;

@MyAnnotation("test")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

编译后就会生成指定包名的指定文件,如下图:

关于注解处理器的辅助接口我们看下面的代码:

public class MyProcessor extends AbstractProcessor {  
  
    private Types typeUtils;  
    private Elements elementUtils;  
    private Filer filer;  
    private Messager messager;  
  
    @Override  
    public synchronized void init(ProcessingEnvironment processingEnv) {  
        super.init(processingEnv);  
        typeUtils = processingEnv.getTypeUtils();  
        elementUtils = processingEnv.getElementUtils();  
        filer = processingEnv.getFiler();  
        messager = processingEnv.getMessager();  
    }  
}  

如上所示,在自定义注解处理器的初始化接口,可以获取到以下 4 个辅助接口:

  • Types: Types 是一个用来处理 TypeMirror 的工具
  • Elements: Elements 是一个用来处理 Element 的工具
  • Filer: 一般我们会用它配合 JavaPoet 来生成我们需要的 .java 文件
  • Messager:Messager 提供给注解处理器一个报告错误、警告以及提示信息的途径

有关注解处理器我另起了一篇 Android 注解处理器解析 来详细讲解。

5.3.5 带有注解的库提供给第三方

以下例子默认用gradle插件 2.2 以上,不再使用 apt。一般使用编译时注解的库,都会有三个 module:

  • xxxx-annotations:java库,定义注解的 module;
  • xxxx-compiler: java库,实现注解器的 module;
  • xxxx-api:android库,提供对外接口的 module。

其中 module xxxx-api 的依赖这么写:

dependencies { 
    annotationProcessor 'xxxx-compiler:1.0.0' 
    compile ' xxxx-annotations:1.0.0' 
    //....others 
}

然后第三方使用时,可以像如下这样依赖:

dependencies {
    ...
    compile 'com.google.dagger:dagger:2.9'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.9'
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值