前言

众所周知,运用 Gson、Jackson 等结构反序列化 JSON 到 Kotlin 类时存在空安全问题和结构器默许值失效的问题,一起常用的 Gson, Moshi 等结构往往在功用上比较强壮,全面,而在功用上却没有很显着的优势。本文将介绍怎么运用 Kotlin 编译器插件打造更安全的 Gson 与更快的 Moshi。

空安全与默许值问题

运用 KCP 打造更安全的 Gson 与更快的 Moshi

如上图所示,咱们在运用 Gson 将 Json 数据反序列化为 User 对象时,会遇到空值安全问题和默许结构参数失效的问题。下面让咱们来探讨一下为何会呈现这样的情况。

运用 KCP 打造更安全的 Gson 与更快的 Moshi

  • 当调用Gson.fromJson办法来反序列化对象时,首先会创立Adapter
  • 在创立Adapter的过程时需求获取类的构建函数,假如存在无参构建函数会直接回来,否则会经过 UnSafe 的办法创立
  • 当运用 UnSafe 办法创立时,就不会调用咱们界说的主结构函数了,默许值天然也就失效了
  • Adapter创立后,接下来便是在Adapter中经过jsonReader读取输入
  • 在读取输入的过程中经过field.set(target, fieldValue)反射赋值,天然也就无法确保空安全了

功用问题

反序列化是 Android 开发中必备且非常高频的需求,因而一个高功用的反序列化结构就非常重要了,那么常用的 Gson 与 Moshi 等结构功用到底怎么样呢?

下面列出运用 Jetpack Microbenchmark 库测验常用反序列化结构的成果,详细测验过程可见:常用 JSON 库功用对比

屡次运转测验成果

运用 KCP 打造更安全的 Gson 与更快的 Moshi

从柱状图能够很显着的看出各个结构的速度对比

  • Kotlin Serialization 看起来是最适合 Kotlin 的反序列化结构,在各个结构中体现最好
  • JSONReader 与 JSONObject 在小数据上体现也不错,在大数据上 JSONReader 与其它结构相差不大,而 JSONObject 因为要将 InputStream 转化成 String,体现较差
  • Moshi 与 Gson 在反序列化速度上距离不大,基本上是一个量级,但比较 JSONReader 等结构则显着较慢

一次运转测验成果

运用 KCP 打造更安全的 Gson 与更快的 Moshi

能够看出,一次运转测验成果与屡次运转测验成果显着不同

  • JSONReader 与 JSONObject 比较其它结构优势显着,在冷发动场景运用这些体系原生 API 应该会有必定优势
  • 在冷发动场景,Moshi 与 Kotlin Serialization 速度差不多,比较 Gson 则略慢

根据上述成果,能够看出 JSONReader 这些原生 API 在冷发动场景有极大优势,在经过充分优化后比较 Gson, Moshi 等库仍然有必定优势。因而,假如要完结更高功用的反序列化,运用 JSONReader 代替 Gson 与 Moshi,应该是个不错的选择。

但是 JSONReader 这些原生 API 运用起来较为费事,需求写很多模板代码,咱们该怎么优化呢?答案便是 Kudos。

Kudos 是什么

Kudos 是 Kotlin utilities for deserializing objects 的缩写。它能够处理运用 Gson、Jackson 等结构反序列化 JSON 到 Kotlin 类时所存在的空安全问题和结构器默许值失效的问题,一起能够简化高功用的反序列化结构 JsonReader 的运用办法。

Kudos 已经在 Github 上开源,开源地址可见:github.com/kanyun-inc/…

Kudos 运用

引入 Kudos 首要分为以下几步

1. 增加插件到 classpath

// 办法 1
// 传统办法,在根目录的 build.gradle.kts 中增加以下代码
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("com.kanyun.kudos:kudos-gradle-plugin:$latest_version")
    }
}
// 办法 2
// 引证插件新办法,在 settings.gradle.kts 中增加以下代码
pluginManagement {
    repositories {
        mavenCentral()
    }
    plugins {
        id("com.kanyun.kudos") version "$latest_version" apply false
    }
}

在项目中启用插件

plugins {
    // 启用 Kudos 插件. 
    // 为被 @Kudos 注解标注的类生成优化版别的无参结构器
    id("com.kanyun.kudos")
}
kudos {
    // 启用 Kudos.Gson. 为被 @Kudos 标注的类一起生成 @JsonAdapter 注解,并增加 kudos-gson 依靠.
    gson = true
    // 启用 Kudos.Jackson. 增加 kudos-jackson 依靠.
    jackson = true
    // 启用 Kudos.AndroidJsonReader. 增加 kudos-android-json-reader 依靠.
    androidJsonReader = true
}

咱们能够在模块等级配置 Kudos 的功用开关

  • 当设置gson = true时就会启用 Kudos.Gson,完结更安全的 Gson
  • 当设置jackson = true时就会启用 Kudos.jackson,完结更安全的 Jackson
  • 当设置androidJsonReader = true时就会启用 Kudos.AndroidJsonReader,简化 JsonReader 的运用

为特定类发动 Kudos 的支撑

@Kudos
data class User(
    val id: Long, 
    val name: String,
    val age: Int = -1,
    val tel: String = ""
)
@Kudos(KUDOS_GSON)
data class User(
    val id: Long, 
    val name: String,
    val age: Int = -1,
    val tel: String = ""
)
  • 关于需求增加 Kudos 解析支撑的类型,直接增加@Kudos注解即可
  • @Kudos注解的类的功用默许与模块配置一致,也能够增加参数例如@Kudos(KUDOS_GSON)完结针对该类只开启特定功用

原理解析:前置知识

编译器插件是什么?

Kotlin 的编译过程,简单来说便是将 Kotlin 源代码编译成方针产品的过程,详细步骤如下图所示:

运用 KCP 打造更安全的 Gson 与更快的 Moshi

Kotlin 编译器插件,经过运用编译过程中供给的各种 Hook 时机,让咱们能够在编译过程中插入自己的逻辑,以到达修正编译产品的意图

Kotlin 编译器插件能够分为 Gradle 插件,编译器插件,IDE 插件三部分,如下图所示

运用 KCP 打造更安全的 Gson 与更快的 Moshi

K2 编译器与 K1 编译器

运用 KCP 打造更安全的 Gson 与更快的 Moshi

编译器前端

Kotlin 编译器能够分为编译器前端与编译器后端两部分,现在 Kotlin 编译器前端有两个版别,一个是老版别的 PSI 版别,一个是即将在 Kotlin 2.0 版别安稳的 FIR 版别。

PSI 即 Program Structure Interface,PSI 原本是 Intellij 渠道对各类编程言语语法树的统一抽象,为了快速完结需求,Kotlin 团队在开发初期复用了 Intellij 渠道已经有的技能积累。而从 Kotlin 1.6 版别开始,为了完结更好的编译器功用,Kotlin团队开始开发一款全新的前端编译器,即 K2 编译器的 FIR 前端。

因而当咱们开发编译器插件时,在触及编译器前端的修正时需求一起适配 K1 与 K2 两个版别。

编译器后端

一起 Kotlin 作为一个多渠道言语,能够将源代码编译成多个渠道的方针代码,比方 Kotlin/Jvm 能够生成 Java 字节码,Kotlin/Js 能够生成 Javascript,Kotlin/Native 能够生成 .so 文件。

不同的编译器后端之间必然有不少逻辑能够同享,因而为了在不同的后端之间同享逻辑,降低支撑新的言语特性的本钱,一起便利后续扩展支撑新的渠道,Kotlin 编译器后端引入了 IR 这一中间层。

不过因为 Kotlin/Jvm 已经在 Kotlin 1.5 版别支撑了 IR,Kotlin/Js 在 Kotlin 1.6 版别支撑了 IR,而 Kotlin/Native 在一开始就支撑 IR,因而咱们在开发编译器插件时,触及修正 IR 时不需求做什么额外的适配。

编译器扩展点

前面提到 Kotlin 编译器插件,经过运用编译过程中供给的各种扩展点,让咱们能够在编译过程中插入自己的逻辑,以到达修正编译产品的意图,下面就介绍一下 Kudos 中运用到的扩展点以及对应的 API

扩展的类名 编译阶段 功用说明 用例
SyntheticResolveExtension 前端 解析生成的类,函数等,可用于给类增加接口或许办法 Parcelize
FirExtensionRegistrar 前端 FIR 扩展,可用于供给代码声明信息与代码查看 Parcelize
StorageComponentContainerContributor 前端 可用于编译期代码查看 Compose
IrGenerationExtension 多渠道 IR 后端 生成与修正 IR Parcelize、Atomicfu、NoArg

总得来说,在触及到修正类或许函数的声明,例如函数的签名信息,给类增加接口等内容时,需求运用到编译器前端的扩展点;而当触及到办法体的内部完结,类的私有特点等内容时,直接生成或修正 IR 即可。

Kudos 原理解析

项目结构

运用 KCP 打造更安全的 Gson 与更快的 Moshi

咱们知道,Kudos 是一个 Kotlin 编译器插件,当然也能够分为 Gradle 插件,编译器插件,IDE 插件三部分,为了支撑 Jackson,Kudos 也供给了 Maven 集成能力。

一起 Kudos 增加了对 Gson, Jackson, JsonReader 等结构的增强,针对这些结构的定制化代码封装在特定的 runtime 模块中,只有开启了对应的功用开关才会引入。

Kudos 怎么确保空安全与默许值?

前面介绍了 Kotlin 编译器插件相关的前置知识,那么 Kudos 到底是怎么确保空安全与默许值的呢?

@Kudos
data class User(
    val id: Long, 
    val name: String,
    val age: Int = -1,
    val tel: String = ""
)

上面这段代码,在编译往后大致相当于

@Kudos
// 假如启用了 com.kanyun.kudos.gson 插件,则生成 @JsonAdapter 注解
@JsonAdapter(value = KudosReflectiveTypeAdapterFactory::class)
data class User(
    val id: Long, 
    val name: String,
    val age: Int = -1,
    val tel: String = ""
) : KudosValidator {
    constructor() { // 生成的默许无参结构器
        super() // 调用父类默许无参结构器
        init<User>() // 调用 User 类内部的 init 块(包括界说在内部的特点初始化)
        this.age = -1 // 运用主结构器的参数默许值初始化特点
        this.tel = "" // 运用主结构器的参数默许值初始化特点
    }
    // 生成的用于校验字段空安全的函数
    override fun validate(status: Map<String, Boolean>) {
        validateField("id", status)
        validateField("name", status)
    }
}
  • Kudos 插件会生成默许无参结构器,在其中运用主结构器的参数默许值初始化特点,因而能够避免默许值失效。
  • 当启用了 Kudos.gson 时,会给类增加@JsonAdapter注解,在自界说的KudosReflectiveTypeAdapterFactory中自界说反序列化逻辑
  • Kudos 插件相同会给类增加KudosValidator接口与validate办法,在validate办法体中会给没有默许值的非空特点增加validateField调用
  • KudosReflectiveTypeAdapterFactory中在反序列化完结后会调用KudosValidatorvalidate办法,以验证声明非空的特点是否不为空,否则会抛出异常

Kudos 怎么简化 JsonReader 的运用

JsonReader 运用起来费事的原因就在于要写很多模板代码,咱们经过编译器插件来生成这些模板代码就能够处理 JsonReader 运用费事的问题。一起当咱们开启 Kudos.AndroidJsonReader, 之前的确保默许值收效与空安全的代码也相同能收效。编译往后的代码如下所示:

@Kudos
data class User(
    val id: Long, 
    val name: String,
    val age: Int = -1,
    val tel: String = ""
) : KudosJsonAdapter {
    private var kudosFieldStatusMap: Map<String, Boolean> = hashMapOf()
    override fun fromJson(jsonReader: JsonReader): User {
        jsonReader.beginObject()
        while (jsonReader.hasNext()) {
            val tmp0 = jsonReader.nextName()
            if (jsonReader.peek() == JsonToken.NULL) {
                jsonReader.skipValue()
                continue
            }
            when {
                tmp0 == "id" -> {
                    <this>.id = jsonReader.nextLong()
                    <this>.kudosFieldStatusMap.put("id", <this>.id != null)
                }
                tmp0 == "name" -> {
                    <this>.name = jsonReader.nextString()
                    <this>.kudosFieldStatusMap.put("name", <this>.name != null)
                }
                // ...
                else -> {
                    jsonReader.skipValue()
                }
            }
        }
        jsonReader.endObject()
        validate(<this>.kudosFieldStatusMap)
        return this
    }
}

kudos-compiler 的完结

运用 KCP 打造更安全的 Gson 与更快的 Moshi

前面咱们介绍了 kudos-compiler 模块要完结的代码,那么作为供给核心能力的模块,kudos-compiler 详细又是怎么完结的呢?

  • KudosCommandLineProcessor 作为编译件插件的入口,注册 pluginId,一起接纳命令行或许 gradle 插件传过来的参数
  • KudosCompilerPluginRegistrar 的效果则是用于注册咱们所需的各种扩展点
  • KudosSyntheticResolveExtension 的效果是在符号引证解析时供给声明信息在符号引证解析时供给声明信息,比方给类增加KudosValidator接口与validate办法
  • KudosFirExtensionRegistrar 的效果是在 K2 解析时供给声明信息与代码查看,与 KudosSyntheticResolveExtension 相似,区别在于两者适用的编译器前端版别不同
  • KudosComponentContainerContributor 的效果是供给代码编译期查看
  • KudosIrGenerationExtension 的效果是在编译产品中供给完结,比方validatefromJson的办法体

功用数据

基于 Kudos 的工作机制不难想到,Kudos 的运转耗时会略微多于对应的 JSON 序列化结构。

运用 Jetpack Microbenchmark 库对 Kudos 与其对应的 JSON 序列化结构进行功用对比能够发现,Kudos.Gson 的耗时为 Gson 的 1.1-1.2 倍, Kudos.Jackson, KudosAndroidJsonReader 的情况相似,在可接受的范围之内。

屡次运转测验成果

small json medium json large json
Gson 412,375 ns 1,374,838 ns 3,641,904 ns
Kudos-Gson 517,123 ns 1,686,568 ns 4,311,910 ns
Jackson 1,035,010 ns 1,750,709 ns 3,450,974 ns
Kudos-Jackson 1,261,026 ns 2,030,874 ns 3,939,600 ns
JsonReader 190,302 ns 1,176,479 ns 3,464,174 ns
Kudos-JsonReader 215,974 ns 1,359,587 ns 4,019,024 ns

一次运转测验成果

small json medium json large json
Gson 3,974,219 ns 4,666,927 ns 8,271,355 ns
Kudos-Gson 4,531,718 ns 6,244,479 ns 11,160,782 ns
Jackson 12,821,094 ns 13,930,625 ns 15,989,791 ns
Kudos-Jackson 13,233,750 ns 15,674,010 ns 18,641,302 ns
JsonReader 662,032 ns 2,056,666 ns 4,624,687 ns
Kudos-JsonReader 734,907 ns 2,362,010 ns 6,212,917 ns

怎么学习 Kotlin 编译器插件

Kotlin 编译器插件现在还没有安稳,没有安稳的 API 与文档,那么咱们该怎么学习 Kotlin 编译器插件呢?

  • 经过 Kotlin 源码学习:因为没有文档,Kotlin 源码实际上便是最新的文档,假如想要学习 KCP,能够先从 Kotlin 官方开发的插件,例如 Parcelize, NoArg 等开始
  • 经过 AI 学习:当咱们在开发编译器插件时,往往是知道想要生成的代码的样式,却因为没有 API 不知道怎么完结,这种场景正是 AI 的用武之地了,咱们能够供给给 AI 想要生成的代码示例,回来编译器插件的完结。
  • 学习《深化实践 Kotlin 元编程》:这本书是现在罕见的针对 Kotlin 编译器插件做了体系介绍的学习资料,除此之外,还介绍了反射,代码生成,程序静态分析,符号处理器等 Kotlin 元编程技能,想要深化学习 Kotlin 元编程的同学都能够了解下。

运用 KCP 打造更安全的 Gson 与更快的 Moshi

总结

本文首要介绍了怎么运用 Kudos ,以及 Kudos 到底是怎么完结的,假如有任何问题,欢迎提出 Issue,假如对你有所帮助,欢迎点赞收藏 Star ~

开源地址

github.com/kanyun-inc/…