Android 编译时注解-初认识

1,996 阅读6分钟

背景

编译时注解越来越多的出现在各大开源框架使用中,比如

JakeWharton/butterknife view

greenrobot/EventBus 事件

square/dagger 依赖注入

类似这样的库在开发和工作中已经越来越多,它们旨在帮助我们在效率为前提的情况下帮助开发者快速开发,节约时间成本。而它们都使用了编译时注解的思想。

正因为如此火热,所以有必要好好学习其中的实现原理,方便解决因为编译时注解导致的问题,同时可将此技术运用到自己的开源库中

思想

编译时注解框架在编写时有相对固定的格式,分包为例

这里写图片描述

格式相对固定,但是也可以灵活变动,比如讲apiannotations结合在一个moudel

moudel中的依赖关系也非常的固定

processors依赖包有api- annotations

app依赖包有 api -annotations-processors

其中除了appandroid moudel以外,其他全部均是java moudel

annotations注解

在讲解annotations注解之前,需要对java和android注解有大致的了解,可以参考我之前的博客

Java-注解详解

Android-注解详解

先初始一个HelloWordAtion注解标注Target为ElementType.TYPE修饰类对象

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

一般一个注解需要对应一个注解处理器,注解处理器在processors处理

processors 注解处理器

对应注解的处理器需要继承AbstractProcessor类,需要复写以下4个方法:

init

init(ProcessingEnvironment processingEnv)会被注解处理工具调用

process

process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。

getSupportedAnnotationTypes

etSupportedAnnotationTypes()这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称

@return 注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合

getSupportedSourceVersion

指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6 `

@return 使用的Java版本

生成注解处理器

AbstractProcessor有了深入的了解,知道核心的初始编译时编写代码的方法及时process,在process中我们通过得到传递过来的数据,写入代码,这里先采用打印的方式,简单输出信息,后续会详细讲解如何自己实现 butterknife功能

public class HelloWordProcessor 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 (Element element : roundEnv.getElementsAnnotatedWith(HelloWordAtion.class)) {

            if (!(element instanceof TypeElement)) {
                return false;
            }

            TypeElement typeElement = (TypeElement) element;
            String clsNmae = typeElement.getSimpleName().toString();
            String msg = typeElement.getAnnotation(HelloWordAtion.class).value();

            System.out.println("clsName--->"+clsNmae+"  msg->"+msg);
        }
        return true;
    }

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

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

到这一步HelloWordAtion对应的注解处理器已经编写完成,这里简单的打印了HelloWordAtion注解的class和注解指定的value信息

准备工作完成以后,app触发调用

@HelloWordAtion("hello")
public class MainActivity extends AppCompatActivity {

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

这里注解注释的类MainActivity并且指定valuehello,到此准备工作就算完成了,这时如果你直接编译或者运行工程的话,是看不到任何输出信息的,这里还要做的一步操作是指定注解处理器的所在,需要做如下操作:

  • 1、在 processors 库的 main 目录下新建 resources 资源文件夹;

  • 2、在 resources文件夹下建立 META-INF/services 目录文件夹;

  • 3、在 META-INF/services 目录文件夹下创建 javax.annotation.process.Processors 文件;

  • 4、在 javax.annotation.process.Processors 文件写入注解处理器的全称,包括包路径;

经历了以上步骤以后方可成功运行,但是实在是太复杂了,博主为了配置这一步也是搞了好久,所以这里推荐使用开源框架AutoService

AutoService

AutoService

直接在Processors中依赖

 compile 'com.google.auto.service:auto-service:1.0-rc2'

使用

@AutoService(Processor.class)
public class HelloWordProcessor extends AbstractProcessor {
xxxxxxx
}

到这里运行程序便可以成功看到后台的输出信息

这里写图片描述

需要切换到右下角的Gradle Console窗口,如果变异不成功可以clean工程以后重新运行

得到需要的数据,下一步当然是将数据写入到java class中,也就是题目所言的编译时注解,如何才能写入,这里需要借助Filer

Filer

AbstractProcessorinit方法中初始Filer

 private Filer filer;  

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

到此我们已经有了写入的类的帮助类,还差代码生成逻辑,这里介绍使用javapoet

javapoet

JavaPoet一个是创建 .java 源文件的辅助库,它可以很方便地帮助我们生成需要的.java 源文件,GitHub上面有非常详细的用法,建议好好阅读相关的使用

javapoet

processors依赖:

compile 'com.squareup:javapoet:1.8.0'

综合上述的技术,仿照javapoet的第一个Example生成如下代码

 @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(HelloWordAtion.class)) {

            if (!(element instanceof TypeElement)) {
                return false;
            }

            TypeElement typeElement = (TypeElement) element;
            String clsNmae = typeElement.getSimpleName().toString();
            String msg = typeElement.getAnnotation(HelloWordAtion.class).value();

            System.out.println("clsName--->"+clsNmae+"  msg->"+msg);

            // 创建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, clsNmae+"-"+msg)
                    .build();

            // 创建HelloWorld类
            TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(main)
                    .build();

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

这里重点讲解process方法,也就是写入代码的方法体,我们在javapoetExample基础上将输出信息改为HelloWordAtion注解获取的信息,到处便完全搞定编译时注解的整个流程,clean以后运行工程,在如下路径下便可看到自动编译生成的HelloWorld

这里写图片描述

到此简单的编译时注解就搞定了,但是编译时注解的自动写入也会导致代码混乱,可能在多次build编译过程中出现文件冲突的情况,所以这里需要引入android-apt

android-apt

android-apt能在编译时期去依赖注解处理器并进行工作,但在生成 APK 时不会包含任何遗留无用的文件,辅助 Android Studio项目的对应目录中存放注解处理器在编译期间生成的文件

android-apt

依赖使用:

根目录build.gradle

 classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

app

apply plugin: 'com.neenbedankt.android-apt'

apt project(':processors')

这里是apt替换compile依赖processors

总结

到此简单的编译时注解就搞定了,但是api模块还没有涉及,别着急接下来的博客中继续扩展,运用掌握的编译时注解和时下主流的butterknife框架,实现一套自己的自定义注入框架中会详细讲解api模块的使用,你会发现原来butterknife很简单,当然可以自由发散,扩展回到自己的任何开源项目中,替换掉反射提高效率。迫不及待的小伙伴可以去GitHub下载源码先自行研究。


专栏

注解-编译运行时注解


源码

下载源码


建议

如果你有任何的问题和建议欢迎加入QQ群告诉我!