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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • 什么是注解注解分类注解作用分类 元注解 Java内置注解 自定义注解自定义注解实现及使用编译时注解注解处理器注解处...
    Mr槑阅读 1,068评论 0 3
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,380评论 25 707
  • 序 对于android应用来说,发布release版本的时候,需要有个正式的签名,这个时候就需要用到jarsign...
    go4it阅读 1,870评论 0 0
  • 谢谢你 对不起 我怕听到这两个词 真的怕了 也许吧 我没有那种命 想和一个我爱的又爱我的人在一起 有那么难吗 我在...
    幸福密码_173f阅读 241评论 0 0