浅析安卓apt

本文主要用来理解编译注解框架。

理解两种关系

编译关系:处于编译关系的库不会被引入应用,但是会进行编译,会获得编译期的上下文环境,一般用这个库来做注解处理和代码生成。对应关键字:

annotationProcessor project(':ioc-compiler')

依赖关系:常见的既会被编译也会被依赖进应用的库。

compile project(':ioc-compiler')

理解三种库:

注解库:提供注解类,既会被编译库依赖,也会被应用依赖。

编译库:依赖注解库,提供编译时生成文件的能力,与主app之间是编译关系。

API库:依赖注解库,提供具体的api接口,负责具体的功能实现,与主app之间是依赖关系。

图示如下:

image.png

auto-service使用方法

gradle文件中 引入compile ‘com.google.auto.service:auto-service:1.0-rc2’,这是google提供的编译处理的库,继承AbstractProcessor自定义编译处理器,然后通过注解@AutoService(Processor.class),就会在编译时自动执行。

这个库以编译库的形式存在,理论上不应该被依赖进工程中。

@AutoService(Processor.class)
// @SupportedAnnotationTypes(Test.class.getCanonicalName()) 把支持的类型添加进去
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class IocProcessor extends AbstractProcessor {

    /** * 日志打印类 **/
    private Messager messager;
    /** * 元素工具类 **/
    private Elements elementsUtils;
    /** 写java文件的类 **/
    private Filer mFiler;
    /** * 保存所有的要生成的注解文件信息 * */
    private Map<String, ProxyInfo> mProxyMap = new HashMap<>();
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        messager = processingEnv.getMessager();
        elementsUtils = processingEnv.getElementUtils();
        mFiler = processingEnv.getFiler();
    }

    /** 
     * 此方法用来设置支持的注解类型,没有设置的无效(获取不到) 同上方注解
     **/
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        // 把支持的类型添加进去
        supportTypes.add(Test.class.getCanonicalName());
        return supportTypes;
    }

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 获取全部的带有Test注解的Element
        Set<? extends Element> elesWidthBind = roundEnv.getElementsAnnotatedWith(Test.class);
        for (Element element : elesWidthBind) {
           // element代表当前注解所代表的元素类型,如果是Field,则为属性元素VariableElement
           // 如果是Class,则代表类元素TypeElement
           // 其它还有:ExecutableElement方法元素、Parameterizable参数元素
            Test bindAnnotation = element.getAnnotation(Test.class); // 获取注解对象
            if (element.getKind() == ElementKind.Class) {
                TypeElement typeElement = (TypeElement) element;
                 String fqClassName = typeElement.getQualifiedName().toString(); // 获取类名
            }
        }
        // 以上代码代表可以编译时获取类和注解的所有信息

        // 以下代码为生成java文件代码
        for (String key : mProxyMap.keySet()) {
            ProxyInfo proxyInfo = mProxyMap.get(key);
            try {
                JavaFileObject jfo = mFiler.createSourceFile(proxyInfo.getProxyClassFullName(), proxyInfo.getTypeElement()); // 创建文件
                Writer writer = jfo.openWriter();
                writer.write(proxyInfo.generateJavaCode()); // 写类文件
                writer.flush();
                writer.close();
            } catch (IOException e) {
                error(proxyInfo.getTypeElement(), "Unable to write injector for type %s: %s ", proxyInfo.getTypeElement(), e.getMessage());
            }
        }

        return true;
    }
}

1、

2、 在AbstractProcessor的核心方法process(Set annotations, RoundEnvironment roundEnv)有两个参数:

Set annotations 包含所有使用的注解的信息,例如BindView,ContentView

RoundEnvironment roundEnv包含所有被注解的元素,例如类,属性等

3、JavaFileObject是java文件对象,可以直接创建,通过流的形式,对文件进行编写。

annotationProcessor和apt的关系

上面提到的编译库:ioc-compiler,应该是以编译的方式引入主工程,annotationProcessor和apt是两个版本的插件,在gradle2.2版本之前,使用apt插件的方式引入,之后便内置为开发框架,具体使用方法如下:

apt

1、project的gradle文件中,配置classpath

buildscript {

    repositories {
        jcenter()
    }

    dependencies {
        classpath "com.android.tools.build:gradle:2.3.0"
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

2、主app目录下的gradle文件中

apply plugin: 'com.neenbedankt.android-apt'
dependencies {
    apt project(':ioc-compiler')
    compile project(':ioc-anotation')
}

执行Build->Rebuild project生效

annotationProcessor

dependencies {
    annotationProcessor project(':ioc-compiler')
    compile project(':ioc-anotation')
}

备注:

ioc-compiler为上文提到的编译库,包含AutoService注解类及类代码生成逻辑。

ioc-anotation为注解库,这个库既会被编译库依赖,也会被app主工程依赖。

注意不要把ioc-anotation的注解类放在ioc-compiler中,这样无法编译通过。

理解Element

    /** 
     * 表示一个程序元素,比如包、类或者方法,有如下几种子接口: 
     * ExecutableElement:表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素 ; 
     * PackageElement:表示一个包程序元素; 
     * TypeElement:表示一个类或接口程序元素; 
     * TypeParameterElement:表示一般类、接口、方法或构造方法元素的形式类型参数; 
     * VariableElement:表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数 
     */  
    public interface Element extends AnnotatedConstruct {  
        /** 
         * 返回此元素定义的类型 
         * 例如,对于一般类元素 C<N extends Number>,返回参数化类型 C<N> 
         */  
        TypeMirror asType();  

        /** 
         * 返回此元素的种类:包、类、接口、方法、字段...,如下枚举值 
         * PACKAGE, ENUM, CLASS, ANNOTATION_TYPE, INTERFACE, ENUM_CONSTANT, FIELD, PARAMETER, LOCAL_VARIABLE, EXCEPTION_PARAMETER, 
         * METHOD, CONSTRUCTOR, STATIC_INIT, INSTANCE_INIT, TYPE_PARAMETER, OTHER, RESOURCE_VARIABLE; 
         */  
        ElementKind getKind();  

        /** 
         * 返回此元素的修饰符,如下枚举值 
         * PUBLIC, PROTECTED, PRIVATE, ABSTRACT, DEFAULT, STATIC, FINAL, 
         * TRANSIENT, VOLATILE, SYNCHRONIZED, NATIVE, STRICTFP; 
         */  
        Set<Modifier> getModifiers();  

        /** 
         * 返回此元素的简单名称,例如 
         * 类型元素 java.util.Set<E> 的简单名称是 "Set"; 
         * 如果此元素表示一个未指定的包,则返回一个空名称; 
         * 如果它表示一个构造方法,则返回名称 "<init>"; 
         * 如果它表示一个静态初始化程序,则返回名称 "<clinit>"; 
         * 如果它表示一个匿名类或者实例初始化程序,则返回一个空名称 
         */  
        Name getSimpleName();  

        /** 
         * 返回封装此元素的最里层元素。 
         * 如果此元素的声明在词法上直接封装在另一个元素的声明中,则返回那个封装元素; 
         * 如果此元素是顶层类型,则返回它的包; 
         * 如果此元素是一个包,则返回 null; 
         * 如果此元素是一个泛型参数,则返回 null. 
         */  
        Element getEnclosingElement();  

        /** 
         * 返回此元素直接封装的子元素 
         */  
        List<? extends Element> getEnclosedElements();  

        /** 
         * 返回直接存在于此元素上的注解 
         * 要获得继承的注解,可使用 getAllAnnotationMirrors 
         */  
        @Override  
        List<? extends AnnotationMirror> getAnnotationMirrors();  

        /** 
         * 返回此元素针对指定类型的注解(如果存在这样的注解),否则返回 null。注解可以是继承的,也可以是直接存在于此元素上的 
         */  
        @Override  
        <A extends Annotation> A getAnnotation(Class<A> annotationType);  
    }

Element可以代表包、类、接口、方法、字段等多种元素种类,具体看getKind()方法中所指代的种类,每个Element 代表一个静态的、语言级别的构件。可以通过getKind判断并强转成对应的子类,可以获得各自特有的方法。

如何理解asType返回的TypeMirror?TypeMirror中也有个getKind方法返回TypeKind可以返回巨化的类型,比如ElementKind为Field,TypeKind返回的就可能是String;ElementKind为Class,TypeKind返回为Error。

TypeElement表示一个类或接口程序元素;PackageElement表示一个包程序元素;ExecutableElement表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素;VariableElement表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数 ;TypeParameterElement表示一般类、接口、方法或构造方法元素的泛型参数。

应用

butterknife使用的就是上面的技术方案,但GreenDao3.x使用了自己开发的插件,将插件引入后,只需要简单的注解,就可以生成所有的相关表代码。猜测在插件代码中,通过编译期获取所有的class文件及注解信息,进行相关代码生成的操作,相当于这是另一条实现思路,但因为插件代码暂时没有开源,所以这里暂时不做具体分析。

https://github.com/greenrobot/greenDAO

https://github.com/JakeWharton/butterknife

JavaPoet

JavaPoet 是一个用来生成 .java源文件的Java API,中央仓库依赖com.squareup:javapoet:1.9.0

先看个样例:

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();

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

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();

javaFile.writeTo(System.out);

生成java文件的内容为:

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

添加一个方法

MethodSpec main = MethodSpec.methodBuilder("main")
    .returns(int.class)
    .addCode(""
        + "int total = 0;\n"
        + "for (int i = 0; i < 10; i++) {\n"
        + "  total += i;\n"
        + "}\n"
        + "return total;")
    .build();
MethodSpec main = MethodSpec.methodBuilder("main")
    .returns(int.class)
    .addStatement("int total = 0")
    .beginControlFlow("for (int i = 0; i < 10; i++)")
    .addStatement("total += i")
    .endControlFlow()
    .addStatement("return total")
    .build();

以上两段代码效果相同,生成代码如下:

int main() {
  int total = 0;
  for (int i = 0; i < 10; i++) {
    total = total * i;
  }
  return total;
}

MethodSpec涉及到的api如下:

returns:设置返回值

addCode:添加一段代码

addStatement:添加一行代码,包含分号和换行

beginControlFlow + endControlFlow:提供换行符和缩进

addParameter:添加参数

举个栗子:

ParameterSpec android = ParameterSpec.builder(String.class, "android")
    .addModifiers(Modifier.FINAL)
    .build();

MethodSpec welcomeOverlords = MethodSpec.methodBuilder("welcomeOverlords")
    .addParameter(android)
    .addParameter(String.class, "robot", Modifier.FINAL)
    .build();

MethodSpec还可以添加构造器。

MethodSpec flux = MethodSpec.constructorBuilder()
    .addModifiers(Modifier.PUBLIC)
    .addParameter(String.class, "greeting")
    .addStatement("this.$N = $N", "greeting", "greeting")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC)
    .addField(String.class, "greeting", Modifier.PRIVATE, Modifier.FINAL)
    .addMethod(flux)
    .build();

生成代码如下:

public class HelloWorld {
  private final String greeting;

  public HelloWorld(String greeting) {
    this.greeting = greeting;
  }
}

添加一个变量

FieldSpec android = FieldSpec.builder(String.class, "android")
    .addModifiers(Modifier.PRIVATE)
    .initializer("$S", "Lollipop")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC)
    .addField(android)
    .addField(String.class, "robot", Modifier.PRIVATE)
    .build();

生成代码如下:

private String android = "Lollipop";
private String robot;

生成一个类

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(today)
    .build();

生成一个抽象类

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
    .addMethod(today)
    .build();

生成一个接口类:

TypeSpec helloWorld = TypeSpec.interfaceBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC)
    .addField(FieldSpec.builder(String.class, "ONLY_THING_THAT_IS_CONSTANT")
        .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
        .initializer("$S", "change")
        .build())
    .addMethod(MethodSpec.methodBuilder("beep")
        .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
        .build())
    .build();

生成代码如下:

public interface HelloWorld {
  String ONLY_THING_THAT_IS_CONSTANT = "change";
  void beep();
}

format

$L => int、float、double

$S => string

$T => type 举个栗子:addStatement("return new $T()", Date.class)

$N 举个栗子:

MethodSpec hexDigit = MethodSpec.methodBuilder("hexDigit")
    .addParameter(int.class, "i")
    .returns(char.class)
    .addStatement("return (char) (i < 10 ? i + '0' : i - 10 + 'a')")
    .build();
    
// 表示方法的引用
addStatement("result[1] = $N(b & 0xf)", hexDigit)

Freemarker

如果觉得Poet太麻烦,还有简单的生成代码方法,就是FreeMarker。

Freemarker 是一款模板引擎,是一种基于模版生成静态文件的通用工具。中央仓库依赖compile 'org.freemarker:freemarker:2.3.23'

获取Template对象并通过Filer对象写入

Configuration config = new Configuration(Configuration.VERSION_2_3_23);
config.setClassForTemplateLoading(getClass(), "/");
Template template = config.getTemplate("xxx.ftl");
Map root = new HashMap();
object.test = "test";
root.put("test", object);
root.put("name", "muyang");
JavaFileObject fileObject = filer.createSourceFile(javaPackage + "." + javaClassName);
template.process(root, fileObject.openWriter());

对一些对象的理解

Filer filer // Filer对象见上文,有提到
String javaPackage // 生成文件所在包
String javaClassName // 生成文件类名
xxx.ftl  // 模板文件
root // dataModel对象,通过一个map承载数据,可以传递任何基本类型和引用类型,用来向tpl传递数据,具体获取数据方式见下文。

xxx.ftl获取dataModel的方式

${name} = muyang
${object.test} = test

直接通过对应名称的方式获取数据。

Tpl简单语法

循环语句

<#list nameList as names>    
  ${names}   
</#list>  

条件语句

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,363评论 6 343
  • 什么是函数重载? 首先想声明下,什么是函数重载,javascript中不存在函数重载的概念,(其实是个伪命题)但一...
    github加星点进来阅读 1,229评论 0 4
  • 「温暖」 带着最微薄的行李和最丰盛的自己流浪世间。 有梦为马,随处可栖。 「逗趣」 小视频·狗中豪 「杂烩」 歌·...
    予缃阅读 137评论 0 0
  • 我陪你度过对酒当歌的夜 我带你走过四下无人的街 穿越人山人海 看尽似锦繁花 路过的风知道 飘过的云知道 我是你 你...
    第1002个故事阅读 214评论 0 1