0%

APT(注解处理器)学习与理解

这段时间通过对注解管理器的学习,跟着网课简单实现了玉观音注解处理,对注解有更多理解。
同时由于该网课是基于kotlin讲解,于是也get了很多kotlin的基本操作之外的炫酷操作
因此这篇文章,重在自身的记录理解

Java注解基础

注解可以理解为标签,用于对代码的标记识别。使用@interface定义一个注解。

元注解

5种元注解,作为基本注解可以用于修饰其他的注解。

@Retention

标示一个注解的存活时间,即生命周期。

  • RetentionPolicy.SOURCE 只在源码阶段保留,编译时被编译器丢弃
  • RetentionPolicy.CLASS 只被保留到编译时,不会被加载到JVM中
  • RetentionPolicy.RUNTIME 可以被保留到运行时,故可以在运行时获取

@Documented

可以将注解中的内容添加到JavaDoc中去。

@Target

限定一个注解的使用场景。一般的注解使用场景不被限定,使用该注解以后使用场景被限定。

  • ElementType.ANNOTATION_TYPE 对一个注解进行注解
  • ElementType.CONSTRUCTOR 对构造方法进行注解
  • ElementType.FIELD 对属性字段进行注解
  • ElementType.LOCAL_VARIABLE 对局部变量进行注解
  • ElementType.METHOD 对方法进行注解
  • ElementType.PACKAGE 对一个包进行注解
  • ElementType.PARAMETER 对一个方法参数进行注解
  • ElementType.TYPE 对一个类型进行注解,如类,接口,枚举

@Inherited

标示该注解标示的类的子类如果没有注解的话,可以继承父类的注解。

@Repeatable

标示该注解可以被使用于同一位置,一般是注解的值可以有多个。实例如下

1
2
3
4
5
6
7
8
@Repeatable(Persons.class)
@interface Person {
String role() default "";
}

@interface Persons {
Person[] value();
}

其中@Repeatable后的Person相当于一个容器注解,即用于存放其他注解的地方,其本身也是一个注解。
容器注解需要设置一个value字段作为注解容器,因此value需要是一个数组,并且其中的注解类型需要是被@Repeatable标示的注解。

注解的属性

注解不存在成员方法,只有成员属性,并可以指定默认值,定义方式如下:

1
2
3
4
5
6
7
@interface Person {
int value();
String msg() default "hello";
}

// 使用
@Person(value=1, msg="hi")

当只有一个成员属性时,可以直接在括号中填写其值无需在写参数名;若没有成员属性

Android中的注解

apt即AnnotationProcessingTool,注释处理工具

  • android-apt和AnnotationProcessor
    android-apt是一位开发者开发的apt框架。AndroidGradle2.2以后提供了annotationProcessor替代android-apt,不支持使用android-apt。但是目前很多项目还是使用的android-apt。
  • 注解处理器是Java层面的东西多所以无法知道kotlin层的东西,因此需要使用Builder::class.java
  • 工具类中的Logger类中的note只能在debugger时才能生效(看看实现)
  • 没有添加kotlin插件支持的模块,将不会处理.kt文件 apply ‘kotlin’
  • build/tmp/kapt3/stubs/debug/*.java 使用kapt后将kotlin代码解释为java文件后存放位置
  • 启用kotlin插件 ‘kotlin-kapt’ 后 通过kapt project() 指定apt模块来指向我们编写的Annotation Process Tool
  • Element 元素 到底是个什么东西? 其实就是注解修饰的对象,可以是成员字段,成员方法,类等
  • 对注解信息进行解析。通过对Element进行解析。TypeElement是Element的一个实现
  • Element.getName获取到的Name对象,其实就是一个CharSequence字符序列
  • Element.getPackage()获取到包名。如果是一个包,就抛出错误
  • 对对成员字段进行保存排序,使用TreeSet,使得成员字段发生增删变化后,其相对顺序仍保持不变,便于开发

  • 自定义Field字段类

    • 设置value并private set,因为其值由注解传入
    • prefix字首 字段的含义
    • TypeMirror? TypeMirror是Java编译时对java类型的一个表示,是所有Type类的基类
    • kotlin “”” “”” 将会把其中的字符串原样输出,也就是说不需要经过转义
    • defaultValue = “”””${optional.stringValue}”””” 是我眼拙了,一共要四个也就是最后把一个叫 “string”
    • 对获取的方法进行解析,

    • ActivityClass 是解析出来的类的模板,持有解析出来的类的信息(代码)

  • 得到是否抽象的标示,没有相关内部字段支持所以可以使用提供的api获取所有修饰符列表,判断其中是否包含abstract

  • 得到是否kotlin代码的标示。由于kapt会对kotlin代码进行解释,出来一份带kotlin注解的代码,所以可以依次进行判断,但是该注解是个接口,只能通过反射获取

  • BuildProcessor

    • 对Builder修饰的元素进行遍历,首先过滤筛查出class的元素
    • 判断该元素是否一个activity,是的话将该element放到一个map中,将element与其activityClass关联起来
    • 对Optional修饰的对象进行遍历,还先筛出Field
    • 通过enclosingElement得到该feild的外部类,通过它在map中找到activityClass,向其中的field(TreeSet)进行赋值
    • 同上操作对Required注解的元素进行activityClass的填充
    • 最后将文件输出

type和kind好像都有类型的意思,具体分别代表着什么

  • 写入到文件(将从注解中获得的信息生成真正的.java文件)
    • 使用ActivityClasss为信息来源,生成的java文件为ActivityBuilder.java,
    • 先生成类信息,不对抽象类进行处理,类名,添加修饰符,基本是public,final。最后得到一个TypeSpec,携带着类的信息
    • 编写一个ConstantBuilder专门生成常量
    • 对生成的常量进行初始化,javapoet中使用”\$S”,S表示字符串
    • 使用JavaFile生成文件
  • 在Process中对文件进行生成,使用一个全局的filer
  • 至此可以生成代码Builder类,其中带有常量字段。因此终于得以一窥该注解处理器的主要目的:首先通过对一个activity中的字段注解进行解析,得到字段名对应的静态常量字段,通过这些字段作为tag,进行activity间的intent通信的媒介。为什么专门生成常量来作为tag,因为后边要通过注解处理器,接管这一通信过程,即舍去大量繁琐的putExtra()过程。因此tag的部分当然需要完全的包装起来。

  • 通过一个start()方法启动intent的包装和activity的启动。创建一个StartMethod类存放持有所有的具体start方法,ContantBuilder来控制生成start函数

  • StartMethod 设置一个list存放所有的field。由于Optional的字段多于3个时将使用实例方法创建start(),所以需要有一个static的标志位。
  • Field列表的填充,从外部传入。此处使用了运算符的重载。定义对Field的操作方法,对该对象进行复制的方法:使用同样的Field,不同的name
  • 编写build()方法。使用name创建方法,添加修饰,返回值,参数。参数需要一个TypeName,向ClassType传入包名路径获取,该类是作者编写的工具类。
  • 方法内语句的编写,需要addStatement()添加声明。使用$T,与$S不同的是,$T可以为我们导包
  • 通过遍历field列表,进一步增加方法参数,将需要的字段作为参数添加到方法中,并逐条进行putExtra()。前边未对field类型名称进行获取,这里补充一下:asJavaTypeName() 这里对name和asJavaType有点不明白
  • $L 直接把值替换,$S是把值加上引号作为字符串的自变量替换
  • 对静态方法进行区分。是静态方法,加上static;不是静态,则加上fillIntent(intent) ????
  • 最后编写启动activity的代码,需要使用Activity的实例去调用startActivity()。于是在runtime模块定义ActivityBuilder类。此处不定义kotlin类,目的在于使用时可以在app中直接依赖而不需要kotlin库。
  • ActivityBuilder使用单例模式,包装startActivity()方法,其实就是当不是Activity的context启动的时候,使用单例启动。所以这里应该可以直接使用startActivity()。这一部分和APT大概关系不大,从其直接被app依赖也可以看出
  • 开始编写StartMethodBuilder,这个类主要用于控制start方法的生成,因为需要根据Optional注解的个数进行调度。
  • 编写build(),首先实例化一个StartMethod,使用ActivityClassBuilder中定义的常量作为方法名。
  • 使用groupBy()根据是否OptionalField进行分类。使用groupBy()后在分为两个list
  • 先将Required注解的字段添加到startMethod()中,表示Required标示的的代码都需要进行处理,即NoOptional不可选的
  • 这里发现了之前写的一个bug,没注意的地方。就是根据OptionalFeild分类时,始终无法识别出OptionalFeild。经过思考,感觉是对field进行写入时的问题,果然,在BuilderProcessor中对Field写入时,判断为Optional时,就直接写入OptaionalField,而我写的Field。
  • 这里又明白一点作者的意图,就是生成多个start()方法,让使用者通过@Optional进行标示以后,可以通过选择不同的start去决定是否给Optional声明赋值。当Optional可选的个数不大于3时,可以通过传入目标字段的方式初始化指定字段;大于3时,通过传入Intent的方式,自构造Intent传入,也就是一般的做法。
  • 这个地方理解有点问题,最终效果应该是,大于3时,生成fillIntent()方法,通过对字段的赋值,使用fillIntent()对字段进行填充。其实大体意义相似,就是实现有点区别。这里需要对是否基本类型进行判断,因为不是基本类型才能使用==Null,基本类型的话就不要判空了
    1
    2
    3
    4
    5
    6
    7
    8
    public ActivityBuilder title(String title){
    this.title = title;
    rerurn this;
    }
    private void fillIntent(Intent intent){
    if(title == null)
    intent.putExtra("title", title);
    }
    关于TypeName不大理解
    一个包装了类型名String的类。ClassName,TypeName的子类。写入方法时,需要用.returns()指明返回值,需要传入TypeName对象。传入包名+类名构造ClassName传入。

MY

  • 因为String不是基本类型,所以需要单独领出来识别。
  • 大写转下划线函数编写。使用了fold()方法辅助,自动为我们遍历集合,我们需要传入一个方法定义对每一个元素的操作

传说中的kotlin特性?

  • AptUtils中的TypeUtils里通过 fun Element.simpleName():String = simpleName.toString(),使得可以在开发中直接用Element调用simpleName()方法
  • 继承Comparabled接口,实现compareTo()方法,使对象可排序(比较)
  • private set 不自动生成setter方法
  • ?: 左值为空则调用右侧代码
  • kotlin “”” “”” 将会把其中的字符串原样输出,也就是说不需要经过转义
  • 运算符重载,使用特定方法名编写扩展方法,并使用operator修饰
  • groupBy() 方法,传入一个函数,根据函数返回的值作为键值分类,返回一个map,键是函数返回的值,对应的值是一个存放相同结果的list,即Map<T, List>

Kotlin学习

方法参数

kotlin的方法参数,可以有默认值,当传参时省略参数时会使用该默认值。
调用参数时,可以使用命名参数的方式传入,增强可读性:fun(arg1 = 2, arg2 = 3)
当返回类型可以由编译器推断,则可以不需要显式声明返回值。有代码块的方法必须显式声明,除非返回Unit

可变长参数

使用vararg修饰,使用数组形式调用。一般置于参数列表最后一个,或者其后的参数使用命名参数传递,或者其后是一个函数类型(lambda)
直接使用数据传递,使用伸展操作符,即*数组名

中缀表示法

调用方法时,使用object fun arg的形式。该方法必须只有一个参数,不能有默认值,不是可变长数组。且必须指明调用者,即this不再可以省略。

顶层函数

在文件顶层声明,即不需要再使用一个类实例调用

局部函数

在函数的内部定义的函数。内部函数可以访问外部函数的变量(闭包)。

扩展函数

为一个第三方库编写一个函数,可以使用该库的对象调用。静态解析。

尾递归优化

对于使用尾递归的函数,kotlin中使用tailrec修饰,编译器会将其优化为循环形式实现的方式。

高阶函数

将函数当做参数或返回值的函数,即高阶函数的参数或返回值是一个函数。

函数类型

(A, B) -> C,表示了函数签名即参数与返回值。Unit返回类型不可省略。

  • (Int)->((Int)-> Unit) == (Int) -> (Int)-> Unit,因为函数类型支持右结合

Lambda表达式与匿名函数

函数字面值。如果函数最后一个参数为函数,则可以将lambda放在参数列表以外。lambda中单个参数可以使用it表示

泛型 声明处型变 类型投影

这一部分官方文档讲的极其复杂。当然没能看懂。看到最后其实就是out和in。跳过了

泛型方法

其实java中也有同样的使用方法只是一直没有注意。一般都是使用类的泛型在方法中调用,但其实可以直接定义泛型方法,

1
2
3
4
fun <T> singleFun(arg: T): T {}

// 扩展函数
fun <T> T.sampleFun(arg: T): T {}

操作符also使用实例

1
2
3
4
5
val method = startMethod.copy(field.name)
method.addField(field)
method.build(typeBuilder)

startMethod.copy(field.name).also { it.addFeild(feild) }.build(typeBuilder)

以上代码实现同样的效果,第二种只有一行且不需要创建引用
由于medthod需要addFeild()处理,但是该方法返回值为空(或返回对象)使得必须使用这种三段式方式实现。also操作符的意义在于此,it为对象本身且返回最后返回值为该对象。

1
startMethod.copy(field.name).staticMethod(false).build(typeBuilder)

而在以上代码中则不需要使用also,因为staticMethod()方法返回对象本身,一直可以直接使用返回值.build()


未完 待续 (╯‵□′)╯︵┻━┻