Jetpack新特性之数据持久化Room 中的KAPT与KSP技术比较之KAPT

3,316 阅读12分钟

框架jetpack新特性

其中我们需要重点关注的有三个领域

  • 架构库及指南
  • 应用的性能优化
  • 用户界面库及指南

具体的这三个领域的解释可以看官方文档,我要说的是其中涉及到的变化的部分一起看 I/O | Jetpack 组件的新特性(官方掘金账号)

在该文中写道:

image.png

如果只想了解其新特性,只需要查看官方文档即可,以下均是对该文档中出现的新概念以及老知识的复习

那么问题来了,KAPT是什么?KSP又是什么?

在了解这个问题之前先看一下Java代码的编译流程,对Java代码的编译流程掌握有助于了解Java Annotation Processing.

一、 Java 编译流程

对于任何语言,其编译原理都是增进语言理解程度的重要环节,在Java中简单讲。可以从javac的角度看简单的编译流程,即Javac将Java源文件编译为符合虚拟机规范的class文件,也是将源代码编译为机器指令的过程,其过程如下:

image.png 上图?部分就是虚拟机,他的作用就是将class 文件编译成能让其他平台识别的二进制文件,这也是Java语言能够跨平台的原因,

image.png

其中上面一行编译流程成为前端编译,不涉及目标机器码相关的代码的生成和优化。

目标代码:java 之所以跨平台的原因就是class二进制文件被编译成了不同的目标代码,促使其跨平台的原因是对应平台的不同虚拟机。

image.png

1.1、 Java编译的7个阶段

5a2ea49a-7e61-46b7-a8d9-c03c5ee0f79c.jpeg

注意从上到下的执行顺序

1.1.1 parse阶段

目的:读取Java源代码文件,做词法、语法分析

  • 词法分析:(lexical analyze)将源代码拆分为一个词法记号(Token),eg:
i = 1 + 2 //----> 被拆分为: i、=、1、+、2五部分

这个过程中会将空格、空行、注释等对程序没有意义的部分排除

  • 语法分析(syntax analyzing):在词法分析的基础上分析单词之间的关系并转换为计算机能够理解的形式,并生成抽象语法 *(AST) *。

抽象语法树:AST(Abstract Syntax Tree),是源代码结构的一种语法表示。是以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构。是后续语义分析、语法校验、代码生成的基础,JSR269就是操作抽象语法树。

1.1.2 enter阶段

目的:解析和填充符号表

符号表是由标识符、标识符类型、作用域等信息构成的记录表,抽象语法树的遍历遇到类型、变量、方法等的定义时,会将他们的信息存储到该表中,提供给后续进行快速查询。

1.1.3 process阶段

目的:做注解处理(JDK1.6开始运行在编译阶段处理注解,Lombok、ButterKnife都是利用该特性做的,通过注解生成目标class 文件,好处是节省代码,比在运行时反射调用性能要好的多)

在JSR269篇节说明,也是改文章的目的。

1.1.4 attr阶段

目的:语义分析的一部分,

1.1.5 flow阶段

目的:处理数据流分析

1.1.6 desugar阶段

目的:解除语法糖

1.1.7 generate阶段

目的:遍历语法树,生成最终的class 文件

具体的编译可以看Java编译原理相关书籍


二、 KAPT

"KAPT (Kotlin Annotation Processing),kotlin 注解处理,是kotiln在M12版本中发布的"

2.1、 在Java中注解的历史以及合适使用

从它的定义来看,其实就是注解处理,在Java编程语言中,Annotation Processing Tool ,在java1.5版本中引入了注解,但是仅支持在运行期处理注解,所以对于开发者来讲能用的场景还是比较单一的,在JDK 1.6中实现了JSR-269规范JSR-269:Pluggable Annotations Processing API(插入式注解处理API)。提供了一组插入式注解处理器的标准API在编译期间对注解进行处理。我们可以把它看做是一组编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环称为一个Round,也就是第一张图中的回环过程。,对于Android开发来讲也可以利用此技术做一些重复工作。

再来看下JSR269是在Java编译的那个阶段使用:

image.png

通过简单的对Java编译对JSR269规范的了解,我们就可以做一些自己想做的操作

2.2 实现注解处理器

我们还需要将我们自定义的注解处理器进行注册。新建resources文件夹,目录下新建META-INF文件夹,目录下新建services文件夹,目录下新建

javax.annotation.processing.Processor

文件,然后将我们自定义注解处理器的全限定名写到此文件,例如我这里:

2.2.1 AbstractProcessor

通过实现Processor接口可以自定义注解处理器,这里我们采用更简单的方法通过继承AbstractProcessor类实现自定义注解处理器。实现抽象方法process处理我们想要的功能。

public class BuilderProcessor extends AbstractProcessor {    

    @Override    

    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {    

        // 处理注解    

     }

  }

除此之外,我们还需要指定支持的注解类型以及支持的Java版本通过重写getSupportedAnnotationTypes方法和getSupportedSourceVersion方法:

class BuilderProcessor : AbstractProcessor() {   

     private val supportedAnnotations =  setOf(Builder::class.java, Required::class.java, Optional::class.java) 

     override fun getSupportedSourceVersion() = SourceVersion.RELEASE_7  

     override fun getSupportedAnnotationTypes() =  supportedAnnotations.mapTo(HashSet<String>(), Class<*>::getName)  

     override fun init(p0: ProcessingEnvironment) {     

        super.init(p0)       

         // 初始化一些工具类

      }    

      override fun process(annotations: MutableSet<out TypeElement>, env: RoundEnvironment): Boolean { 

             // 处理注解   

       }

}

2.2.2 抽象语法树操作API

Names

Names类提供了访问标识符Name的方法,它最常用的方法是fromString,用来从一个字符串中获取Name对象,此方法声明如下:

public Name fromString(String s) { 

   return table.fromString(s);

   }
JCTree

JCTree是语法树元素的基类,包含一个重要的字段pos,该字段用于指明当前语法树节点(JCTree)在语法树中的位置,因此我们不能直接用new关键字来创建语法树节点,即使创建了也没有意义。此外,结合访问者模式,将数据结构与数据的处理进行解耦.部分源码如下所示

public abstract class JCTree implements Tree, Cloneable, DiagnosticPosition {



    public int pos = -1;



    ...



    public abstract void accept(JCTree.Visitor visitor);



    ...

}

我们可以看到JCTree是一个抽象类,这里重点介绍几个JCTree的子类:

  1. JCStatement:声明语法树节点,常见的子类如下
  • JCBlock:语句块语法树节点
  • JCReturn:return语句语法树节点
  • JCClassDecl:类定义语法树节点
  • JCVariableDecl:字段/变量定义语法树节点
  1. JCMethodDecl:方法定义语法树节点
  2. JCModifiers:访问标志语法树节点
  3. JCExpression:表达式语法树节点,常见的子类如下
  • JCAssign:赋值语句语法树节点
  • JCIdent:标识符语法树节点,可以是变量,类型,关键字等等
TreeMarker

TreeMaker用于创建一系列的语法树节点,我们上面说了创建JCTree不能直接使用new关键字来创建,所以Java为我们提供了一个工具,就是TreeMaker,它会在创建时为我们创建的JCTree对象设置pos字段,所以必须使用上下文相关的TreeMaker对象来创建语法树节点。

具体的API介绍可以参照,TreeMakerAPI 重点介绍如下方法

TreeMaker.Modifiers

方法用于创建访问标志语法 节点(JCModifiers)

public JCModifiers Modifiers(long flags) {

    return Modifiers(flags, List.nil());

}



public JCModifiers Modifiers(long flags,

    List<JCAnnotation> annotations) {

        JCModifiers tree = new JCModifiers(flags, annotations);

        boolean noFlags = (flags & (Flags.ModifierFlags | Flags.ANNOTATION)) == 0;

        tree.pos = (noFlags && annotations.isEmpty()) ? Position.NOPOS : pos;

        return tree;

}

其中参数flag表示访问标志,annotations表示注解列表。

flag可以使用枚举类com.sun.tools.javac.code.Flags来表示,例如要表示public static final可以这样用:

treeMaker.Modifiers(Flags.PUBLIC + Flags.STATIC + Flags.FINAL);
TreeMaker.ClassDef

TreeMaker.ClassDef用于创建类定义语法树节点(JCClassDecl),源码如下:

public JCClassDecl ClassDef(JCModifiers mods,

    Name name,

    List<JCTypeParameter> typarams,

    JCExpression extending,

    List<JCExpression> implementing,

    List<JCTree> defs) {

        JCClassDecl tree = new JCClassDecl(mods,

                                     name,

                                     typarams,

                                     extending,

                                     implementing,

                                     defs,

                                     null);

        tree.pos = pos;

        return tree;

}

参数说明:

  1. mods:访问标志,可以通过方法TreeMaker.Modifiers来创建
  2. name:类名
  3. typarams:泛型参数列表
  4. extending:父类
  5. implementing:实现的接口
  6. defs:类定义的详细语句,包括字段、方法的定义等等
TreeMaker.MethodDef

TreeMaker.MethodDef用于创建方法定义语法树节点(JCMethodDecl),源码如下:

public JCMethodDecl MethodDef(JCModifiers mods,

    Name name,

    JCExpression restype,

    List<JCTypeParameter> typarams,

    List<JCVariableDecl> params,

    List<JCExpression> thrown,

    JCBlock body,

    JCExpression defaultValue) {

        JCMethodDecl tree = new JCMethodDecl(mods,

                                       name,

                                       restype,

                                       typarams,

                                       params,

                                       thrown,

                                       body,

                                       defaultValue,

                                       null);

        tree.pos = pos;

        return tree;

}



public JCMethodDecl MethodDef(MethodSymbol m,

    Type mtype,

    JCBlock body) {

        return (JCMethodDecl)

            new JCMethodDecl(

                Modifiers(m.flags(), Annotations(m.getAnnotationMirrors())),

                m.name,

                Type(mtype.getReturnType()),

                TypeParams(mtype.getTypeArguments()),

                Params(mtype.getParameterTypes(), m),

                Types(mtype.getThrownTypes()),

                body,

                null,

                m).setPos(pos).setType(mtype);

}

参数说明:

  1. mods:访问标志
  2. name:方法名
  3. restype:返回类型
  4. typarams:泛型参数列表
  5. params:参数列表
  6. thrown:异常声明列表
  7. body:方法体
  8. defaultValue:默认方法(可能是interface中的哪个default)
  9. m:方法符号
  10. mtype:方法类型。包含多种类型,泛型参数类型、方法参数类型、异常参数类型、返回参数类型。

其中返回类型restype填写null或者treeMaker.TypeIdent(TypeTag.VOID)都代表返回void类型。

TreeMaker.VarDef

TreeMaker.VarDef用于创建字段/变量定义语法树节点(JCVariableDecl),源码如下:

public JCVariableDecl VarDef(JCModifiers mods,

    Name name,

    JCExpression vartype,

    JCExpression init) {

        JCVariableDecl tree = new JCVariableDecl(mods, name, vartype, init, null);

        tree.pos = pos;

        return tree;

}



public JCVariableDecl VarDef(VarSymbol v,

    JCExpression init) {

        return (JCVariableDecl)

            new JCVariableDecl(

                Modifiers(v.flags(), Annotations(v.getAnnotationMirrors())),

                v.name,

                Type(v.type),

                init,

                v).setPos(pos).setType(v.type);

}

参数说明:

  1. mods:访问标志
  2. name:参数名称
  3. vartype:类型
  4. init:初始化语句
  5. v:变量符号

2.2.3 常用类介绍

AnnotationMirror

AnnotationMirror用于表示一个注解,其中提供了两个方法分别用于获取注解类型和注解的值,如下所示。

public interface AnnotationMirror {



    /**

     * 获取此注解的ElementType

     */

    DeclaredType getAnnotationType();



    /**

     * 获取此注解的所有Element的值

     */

    Map<? extends ExecutableElement, ? extends AnnotationValue> getElementValues();

}
Element

源代码的每一个部分都是一个特定类型的Element,也就是说Element代表源文件中的元素,例如包、类、字段、方法等。

Element类是一个接口,由Element衍生出来的扩展类共有五种,分别是:

  • PackageElement:表示一个包程序元素,提供对有关包及成员的信息的访问。
  • TypeElement:表示一个类或者接口程序元素,提供对有关类型及其成员信息的访问。
  • TypeParameterElement:表示一个泛型元素。
  • VariableElement:表示一个字段、enum常量、方法或者构造方法的参数、局部变量或异常参数。
  • ExecuteableElement:表示某个类或者接口的方法、构造方法或初始化程序(静态或者实例)。

2.2、 在kotlin中, 官方设计时提出了3中方案:

  • 为kotlin重新实现一套JSR-269协议;
  • 从kotlin源码中生成Java源码;
  • 将kotlin 代码编译成Java编译器可识别的二进制文件,在Java编译器扫描注解信息之前交给Processor之前,kotlin 编译器注入其扫描的注解信息;

通过上述的三种方案,我们大至已经知道kapt到底是什么了,在了解Java的Annotation Processing 之后,kotlin的注解更加好理解,

三、那么KAPT 最终使用了那种方案,以及它的优缺点是什么呢

3.1 为kotlin重新实现一套JSR-269协议

“kotlin 的宗旨其中有一句是与Java 100% 兼容” 如果使用其方式将直接破坏这一宗旨

3.2 从kotlin源码中生成Java源码

也就是将你的Kotlin代码逻辑给翻译成可执行的Java代码,并将它们加入到javac的classpath中,最后运行注解处理器,这种方案带来的工作量和难度是巨大的

3.3 将kotlin 代码编译成Java编译器可识别的二进制文件

由于在一开始Kotlin就已经将代码编译好,所以代码无法引用Processor生成的类,而且仅在源文件中保留的注解(RetentionPolicy.SOURCE)将被忽略。

使用该方案的弊端也是实现它的方式:

  1. kotlin 代码不能引用Processor生成的代码;

这个可以通过事先定义好这些生成类的路径与方法名然后在原吗中通过反射去调用,听着就很扯

  1. JetBrains团队做了一些优化:生成源码的"存根类"以支持对生成代码的引用;当然上面提到的问题依然没有解决,只是把生成"存根类"作为可选配置由用户自己决定是否使用该功能而已,而且这种对生成代码的引用并不完美:虽然生成的"存根类"中没有方法体,所以方法体内对尚未生成的代码进行引用不会报错,但是方法签名中如果包含了对这些代码的引用(参数类型或者返回类型),最终Processor生成的Java代码中会引用一些不存在的类从而导致编译器报错(unresolved reference)。 后来相继推出了kapt2以及kapt3,kapt2通过包装Intellij平台的抽象语法树实现了JSR-269来克服上面提到的限制,但是Intellij平台没有针对这些实现进行优化,导致某些时候注解处理器可能会非常慢;kapt3替代了kapt2的实现,直接从Kotlin代码生成javac的Java AST,后面的步骤直接走javac原本的流程即可。 到目前为止kapt3已经解决上面提到的大部分问题,但仍然存在一些不太友好的地方,比如使用kapt3生成了Kotlin代码在AndroidStudio中是不会加入到Kotlin编译器中编译的,需要自己在build.gradle中配置类似下面这样的代码
afterEvaluate {
    android.applicationVariants.all { variant ->
        if (variant.buildType.name == 'debug') {
            def kotlinGenerated = file("$buildDir/generated/source/kaptKotlin/${variant.name}")
            variant.addJavaSourceFoldersToModel(kotlinGenerated)
            return
        }
        ...
    }
}

总的来说,KAPT是kotlin 为了兼容kotlin 的Annotation Processing

后面分析KSP技术

参考链接:

KAPT(Annotation Processing for Kotlin)是什么: www.jianshu.com/p/8c3437006…

官方博客相关文章:blog.jetbrains.com/kotlin/2015…
blog.jetbrains.com/kotlin/2015…
Medium相关文章:medium.com/@workingkil…
medium.com/@daptronic/…