安卓编译时注解的应用(一)

Java中的注解是一个非常简洁的东西。使用它可以简化上层代码,让你的业务逻辑变的清晰明了。
注解分运行时级别的注解,编译时级别的注解以及源码级别的注解。运行时注解需要用到反射相关的知识,有性能的损耗,今天我们来讲讲编译时的注解,它的作用可以让你的程序在编译过程中通过代码自动生成一些类然后调用,如此就没有反射什么事了,性能也就提升了。Class级别的注解没使用过这里不做探讨了。

使用编译时注解很简单,在创建注解的时候只需要指定它的保留策略为Source级别即可(经过测试指定为CLASS级别也可以)。
下面这段代码创建了一个Class级别的注解:

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface AuthorityFail {

    int value();
}

这个注解表面是标注在方法上使用的,保留策略为Class级别。

现在步入本文的正题,如果你连注解是什么都还不清楚,建议先去了解了解注解在来看下文这样会更好些。通过阅读本文您将一步一步的明白编译时注解的到底是怎么一回事,会让你避免很多坑,之后在回过头来回想一下可能就会觉得原来Butterknife也就那么一回事。

  • 配置编译时注解
    在安卓中配置编译时注解很方便,谷歌已经帮我们做好了很多措施了。下面一步一步讲解
    首先打开AndroidStudio 创建一个Project,我这里的名字叫做AndZilla,然后在其下创建两个Module,注意这两个Module类型是javalibrary的。这里我创建了一个名为ioc-annotation和ioc-compiler的两个javalibrary。一个是用来存放注解的,我们的下面例子中用的注解就是存放在这个模块下的,另一个是用来编译注解库的,也就是让我们自定义的注解生效的模块。其次在创建一个android library,我这里叫做ioc-api.这个模块是给你的应用调用使用的。
编译时注解的结构
  • 万事起于一个HelloWorld
    学每一项技术之前都会先去做一个HelloWorld。这里也不例外我们就从一个HelloWorld开始,先让代码帮我们自动创建一个HelloWorld类。
1.我们先在ioc-annotation模块下自定义个注解取名为HelloWorld,里面有一个String类型的属性。
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface HelloWorld {

    String say();
}
2.在ioc-compiler模块下创建一个继承自AbstractProcessor的类,这里取名为HelloWorldProcessor。

这里先来介绍一下这个类,这是一个注解处理器类。在编译代码的时候会执行这个类的。继承这个类之后我们需要实现里面的process方法,这个方法就是来处理我们的注解用的。
此类中还是其他几个方法这里简单介绍一下:
public void init(ProcessingEnvironment processingEnv)
这个方法一次编译过程中只会执行一次,在这里我们可以初始化或者获取自己所需要的一些参数。
public Set<String> getSupportedAnnotationTypes()
实现此方法告诉这个注解处理类,需要处理的注解参数。
public SourceVersion getSupportedSourceVersion()
这个方法用来指定只是的JDK版本。
public Set<String> getSupportedOptions()
这个方法返回该注解处理类所支持的参数。其实上面所说的getSupportSourceVersiongetSupportedSourceVersion都可以用注解来代替配置。
该方法的英文介绍:If the processor class is annotated with SupportedOptions, return an unmodifiable set with the same set of strings as the annotation. If the class is not so annotated, an empty set is returned.
如果这个处理类使用注解配置选项,这返回这个配置集合,否则返回空集合。
public abstract boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
这个方法用来处理我们定义的注解。
现在我们在ioc-compiler的gradle文件中引入如下依赖:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.google.auto.service:auto-service:1.0-rc2'//配置谷歌AutoService注解,使用它可以省掉很多配置
    compile 'com.squareup:javapoet:1.8.0'//这是一个简单强大的代码创建工具
    compile project(':ioc-annotation')//引入我们自己的注解模块
}

同步完成之后在我们的HelloWorldProcessor上加上@AutoService(Processor.class)的标注并加上下面的哦配置代码:

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

    private Elements elements;//辅助工具类
    private Filer filer;//用来写入文件用

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        elements=processingEnvironment.getElementUtils();
        filer=processingEnvironment.getFiler();
    }

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations=new HashSet<>();
        annotations.add(HelloWorld.class.getSimpleName());
        return annotations;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return true;
    }
}

现在我们重点来处理pricess中的功能。
我们在process方法中加入这些代码,我们把一些关键信息打印出来看看:

 Set<TypeElement > typeElements=ElementFilter.typesIn(roundEnvironment.getElementsAnnotatedWith(HelloWorld.class));
 for (TypeElement typeElement:typeElements){
      System.out.println("#######"+typeElement.getSimpleName());
      System.out.println("#######"+typeElement.getEnclosingElement().asType());
      System.out.println("#######"+typeElement.getQualifiedName());
      System.out.println("#######"+typeElement.getSuperclass().toString());
}

之后我们在app模块中的gradle文件下加入如下依赖:

compile project(path: ':ioc-annotation')
annotationProcessor   project(':ioc-compiler')

并在MainActivity类上加入@HelloWorld注解

@HelloWorld(say="MainActivity ")
public class MainActivity extends AppCompatActivity {

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

然后,clean project,build project,如果在Gradle Console中可以看到输出的信息:

#######MainActivity
#######ggx.com.iocdemo
#######ggx.com.iocdemo.MainActivity
#######android.support.v7.app.AppCompatActivity

表明你已经成功了。从输出信息中我们看到了很多信息。这些信息都是来自于Element对象,Element类的结构如下:

Element类及其子类

ExecutableElement:表示可执行的元素如构造方法,普通方法。
PackageElement:表示包元素
TypeElement:表示类元素
TypeParameterElement:表示参数化类元素 也就是泛型。
VariableElement:表示属性字段元素。
这里我只简单介绍一下,每种元素都对应一个内容,具体的API可以参考这篇Element API文档
在上面的例子中我用的是TypeElement元素,因为我的注解是标注在类上的。通过TypeElement可以获取到被标注类的所在包,类名,实现的接口以及继承的父类等。

3.生成HelloWorld类

这里我们创建的HelloWorld类名规则是按照注解标注类的类名加$$HelloWorld,并添加say方法,方法体中打印出注解中say的值。
我们将process中的代码替换成如下:

 Set<TypeElement > typeElements=ElementFilter.typesIn(roundEnvironment.getElementsAnnotatedWith(HelloWorld.class));
for (TypeElement typeElement:typeElements){
      String say=typeElement.getAnnotation(HelloWorld.class).say();
       MethodSpec.Builder sayBuilder=MethodSpec.methodBuilder("say")
               .addModifiers(Modifier.PUBLIC)
               .returns(void.class)
               .addStatement("$T.out.println($S)",System.class,say);
        TypeSpec clazz=TypeSpec.classBuilder("HelloWorld$$"+typeElement.getSimpleName())
                .addModifiers(Modifier.PUBLIC)
                .addMethod(sayBuilder.build()).build();
         String packageName=elements.getPackageOf(typeElement).getQualifiedName().toString();
         try {
              //将filer替换成System.out就可以在控制台看到输出信息
              JavaFile.builder(packageName,clazz).build().writeTo(filer);
          } catch (IOException e) {
              e.printStackTrace();
}

对javapoet这个工具陌生的请点击这里自行脑补官方的readme对使用方法介绍的非常清楚
现在点击build之后我们可以在build/generated/source/apt/debug或者release文件夹下看到生成的代码:

package ggx.com.iocdemo;
import java.lang.System;
public class HelloWorld$$MainActivity {
  public void say() {
    System.out.println("MainActivity");
  }
}

至此,整套流程介绍完了,不知道有没有看明白了,不明白的话可以在评论去交流也可以加群号(下面有)。既然能生成HelloWorld类那么就可以生成你想要的一切类,没有做不到只有想不到。Butterknife也是通过自动创建类,来代替你手写findViewById。后面讲解基于编译时注解的实现。

欢迎共同探讨更多安卓,java,c/c++相关技术QQ群:392154157

推荐阅读更多精彩内容