Android Json 解析你还在用 fastjson 吗?

前言

在日常开发中,处理 Json 格式的数据是一项常见的工作,无论是做什么类型的业务,在各种类型的 UI 之下,数据才是驱动一切进行的源头。随着技术的发展,在 Android 开发中用于 Json 解析的库也越来越多,甚至随着 Kotlin 和 Jetpack Compose 的发展,借助其语法糖和独特的开发方式,出现了专有的库。同时 Android 官方也提供对于数据序列化的更多支持。下面我们就一些常用的 Json 解析做一下比较,了解一下各自的特点,以便在日常开发中做出合适的选择。

以下举例以自己日常开发中常用或者用过的库为例

Json 解析

对于 Android 开发来说,Json 解析最有名的库莫过于 fastjson 了,相比系统自动的 Json 库提供了更友好的 API,方便开发者快速解析服务端返回的 Json 数据。尤其是在业务复杂的场景,比如电商类 App 的购物车、商品详情页、订单页这样的接口,返回的数据结构嵌套多,甚至可以说恐怖。而有了类似 fastjson 这样的库,只要定义好了数据类(也就是 JavaBean/JavaModel,从那个年代过来的人不同公司似乎有不同的叫法,但意思是相通的),一个简单的接口就可以快速将复杂的 json 字符串序列化为对象。同时,Google 官方也提供了 Gson 这样的库。 fastjson 也一度因为安全问题而被大家质疑和恐慌性的弃用。

而后,随着 Kotlin 成为主流,又出现了 Klaxon,Moshi 这样基于 Kotlin 的 Json 库。同时,Android 官方也开始在这个方面发力,提供了 kotlinx.serialization​ 插件,通过给数据类添加注解的方式,非常友好的实现了数据解析的功能。

下面我们就结合实例总结和对比一下这些库各自的特点。

以下实例数据类结构如下

kotlin 复制代码
class School {
    class Location {
        var lat = 0f
        var lng = 0f
    }

    var name: String? = null

    var province: String? = null

    var city: String? = null

    var area: String? = null

    var address: String? = null

    var location: Location? = null
}

data class Schools(
    var id: Int = 0,
    var province: String? = null,
    var schoolList: List<School>? = null,
    var type: Int = 0,
    var code: String? = ""
)

这是一个全国各省市学校信息的数据,以省市为单位依次列出每个地区的所有学校地址。所有省市的 Schools 构成一个大的列表,原始 json 字符串大约有 5.6 MB,下面我们以反序列这个字符串为例依次对比一下上面提到的这些 Json 库的用法。

fastjson 和 Gson

fastjson 和 Gson 大家应该非常熟悉了,具体依赖就不再列出,简单看一下实现。

kotlin 复制代码
private fun parseWithFastJson(json: String): List<Schools>? {
    val s = System.currentTimeMillis()
    val list = JSONObject.parseArray(json, Schools::class.java)
    Log.e(TAG, "parseWithFastJson cost ${System.currentTimeMillis() - s}")
    return list
}

private fun parseWithGson(json: String): List<Schools>? {
    val s = System.currentTimeMillis()
    val gson = Gson()
    val list: List<Schools> = gson.fromJson(json, object : TypeToken<List<Schools>>() {}.type)
    Log.e(TAG, "parseWithGson cost ${System.currentTimeMillis() - s}")
    return list
}

这两者相对而言,fastjson 在速度和语法友好性上更占优。尤其对于列表的处理,Gson 的语法有点晦涩,记得当时刚开始学习 Kotlin 的时候,完全不知道该怎么写,因为原先 Java 的写法就很别扭,导致语法转换一直搞不定。

moshi

moshi 是由 Android 开发者非常熟悉的 square 出品的,就是那个搞出 okhttp,retrofit,leakcanary 的牛逼公司。

首先需要添加以下依赖

gradle 复制代码
    implementation("com.squareup.moshi:moshi:1.15.2") // 核心库
    implementation("com.squareup.moshi:moshi-kotlin:1.15.2") // Kotlin 扩展(支持数据类)
    kapt("com.squareup.moshi:moshi-kotlin-codegen:1.15.2") // 代码生成(替代反射)

具体实现

kotlin 复制代码
private fun parseWithMoshi(json: String) :List<Schools>? {
    val s = System.currentTimeMillis()

    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory()) // 支持 Kotlin 数据类
        .build()
    val adapter = moshi.adapter<List<Schools>>(Types.newParameterizedType(List::class.java, Schools::class.java))
    val list = adapter.fromJson(json)
    Log.e(TAG, "parseWithMoshi cost ${System.currentTimeMillis() - s}")
    return list
}

一看 moshi 的用法就会有一种似曾相识的感觉,square 一贯的风格,builder 模式,提供更多的灵活性,让使用者用配置的方式实现自己想要的结果。但是似乎又有些繁琐了,尤其是列表结构的常见,还得单独添加一下适配器。

klaxon

klaxon 似乎纯粹是为了借 kotlin 的东风,完全用 kotlin 实现的 Json 解析库,曾经名噪一时,但是已经停止维护了,这里列一下纯粹当做历史记录。

kotlin 复制代码
private fun parseWithKlaxon(json: String): List<Schools>? {
    val s = System.currentTimeMillis()
    val list = Klaxon().parseArray<Schools>(json)
    Log.e(TAG, "parseWithKlaxon cost ${System.currentTimeMillis() - s}")
    return list
}

kotlinx.serialization​

kotlinx.serialization 是 Kotlin 官方提供的序列化库,支持 JSON、Protobuf、CBOR 等多种格式,​​无需反射​​(通过编译时代码生成实现类型安全)

下面是他的用法

  1. 在 app/build.gradle 添加插件
gradle 复制代码
plugins {
    id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.10'
}
  1. 添加依赖
gradle 复制代码
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
  1. 为数据类添加 @Serializable 注解
kotlin 复制代码
@Serializable
data class Schools(
    var id: Int = 0,
    var province: String? = null,
    var schoolList: List<School>? = null,
    var type: Int = 0,
    var code: String? = "aa"
) 

这里当我们给 Schools 添加注解时,如果其内部含有未添加注解的类 Android Studio 就会爆红提示,比如这里的 List<School> 直到所有需要序列化的类都添加完毕。

  1. 解析数据
kotlin 复制代码
fun parseWithKotlinxSerializable(jsonStr: String): List<Schools>? {
    val s = System.currentTimeMillis()
    val list = Json.decodeFromString<List<Schools>>(jsonStr)
    Log.e("Coroutines", "parseWithKotlinxSerializable cost ${System.currentTimeMillis() - s}")
    return list
}

可以看到其语法也很友好,见名知意,不需要额外的操作。

下面我们就来简单对比一下这几个实现的耗时。

kotlin 复制代码
    private fun useCoroutine2(block: (String) -> Unit) {
        
        CoroutineScope(Dispatchers.IO).launch {
            Thread.currentThread().name.lg(TAG)
            val start = System.currentTimeMillis()
            val json = IOTool.readStrFromAssets("school.json", context)
            Log.e(TAG, "read json cost ${System.currentTimeMillis() - start}")
            block(json)

    }

    useCoroutine2 { parseWithKlaxon(it) }
    useCoroutine2 { parseWithFastJson(it) }
    useCoroutine2 { parseWithGson(it) }
    useCoroutine2 { parseWithMoshi(it) }
    useCoroutine2 { parseWithKotlinxSerializable(it) }

我们从 assets 目录读取 json 字符串之后,依次在 IO 线程调用通过参数传入的高阶函数来比较不同实现的差异。

shell 复制代码
Coroutines                               E  parseWithGson cost 1566
Coroutines                               E  parseWithFastJson cost 3137
Coroutines                               E  parseWithKotlinxSerializable cost 3414
Coroutines                               E  parseWithMoshi cost 8761
Coroutines                               E  parseWithKlaxon cost 78415

可以看到 Gson 最快,Klaxon 最慢,甚至慢的有些离谱,几乎是其他库的 10多倍(怪不得放弃维护了,估计没得救了)

但是号称最快的 fastJson 怎么败给了 Gson 呢?是哪里出了问题吗。这个问题,我们把数据类的定义从 kotlin 修改为 java 之后再看看

kotlin 复制代码
private fun parseWithFastJson1(json: String): List<com.engineer.imitate.model.java.Schools>? {
    val s = System.currentTimeMillis()
    val list = JSONObject.parseArray(json, com.engineer.imitate.model.java.Schools::class.java)
    Log.e(TAG, "parseWithFastJson1 cost ${System.currentTimeMillis() - s}")
    return list
}
shell 复制代码
Coroutines                               E  parseWithFastJson1 cost 4547
Coroutines                               E  parseWithGson cost 4903
Coroutines                               E  parseWithFastJson cost 6523
Coroutines                               E  parseWithKotlinxSerializable cost 7104
Coroutines                               E  parseWithMoshi cost 12603
Coroutines                               E  parseWithKlaxon cost 81603

可以看到改成 Java 之后 fastJson 明显变快了。当然,这其中的原因就是 Kotlin data class 导致的,由于构造函数的实现和 Java 有差异,所以在解析时需要用其他方式实现,因此会耗时一些。

当然,以上数据对比只能表现出单次结果的相对差异,并无法作为这些库性能优劣的指标 。但是,依然有参考意义。相对值之间差异较大的时候,其性能优劣就已经很明显了。日常开发实际使用时,用于序列化的 gson/json/moshi 实例应该尽可能的复用,而不是每次创建新的,这里只是为了方便举例。

综合对比

最后,我们用表格梳理一下上述几个库从更多维度再看看各自的差异。(借助 AI 生成, 准确性值得信赖)

维度 Klaxon Fastjson Gson Moshi kotlinx.serialization
语言支持 Kotlin Java Java/Kotlin(需适配) Java/Kotlin(需扩展库) Kotlin 原生
编译时安全 ❌ 反射 ❌ 反射 ❌ 反射 ✅ 可选(通过 kapt 代码生成) ✅ 原生支持(无反射)
空安全支持 ❌(需额外配置) ✅(需 Kotlin 扩展库) ✅ 原生支持
默认字段名映射 支持(需注解) 支持(@JSONField 支持(@SerializedName 支持(@Json 支持(@SerialName
性能(序列化) 中等(≈1.5x Gson) 极快(≈3x Gson) 基准(1x) 快(≈2x Gson,代码生成模式) 中等(≈1.2x Gson)
性能(反序列化) 中等 极快 慢(≈0.8x Klaxon) 中等
依赖大小 轻量(≈200KB) 中等(≈500KB) 轻量(≈250KB) 轻量(≈300KB,含 Kotlin 扩展) 轻量(≈200KB)
多格式支持 仅 JSON 仅 JSON 仅 JSON 仅 JSON(需插件) ✅ JSON/Protobuf/CBOR 等
Kotlin 数据类支持 ✅ 原生友好 ❌(需手动处理) ❌(需适配) ✅(需 Kotlin 扩展库) ✅ 原生完美支持
泛型支持 ✅(需复杂类型声明) ✅ 原生支持
安全性 高(少历史漏洞) 低(多次高危漏洞,需更新版本) ✅ 最高(无反射)
典型使用场景 纯 Kotlin 小项目 Java 高性能后端 Android/通用 Java 项目 Android/Kotlin 项目 Kotlin 全平台(KMM/Android)

关键数据参考(基准测试) (源自网络)

测试环境:Kotlin 1.9 / JDK 17,数据集:1000 条嵌套 User 对象

(数值越低越好,Gson 为基准 1.0):

序列化时间 反序列化时间 内存占用
Fastjson 0.3x 0.4x 中等
Moshi (代码生成) 0.5x 0.6x
kotlinx.serialization 0.8x 0.9x 最低
Klaxon 1.5x 1.3x
Gson 1.0x 1.0x 中等

小结

通过以上内容和对比结果可以看到,没有最好的,只有最合适的。长远来说,在 Android 上 kotlinx.serialization 似乎是不错的选择,有官方支持。而且他还可以用于 protobuf 之类的其他语言,而且还有 Kotlin 全平台的支持,对于快平台开发来说也是一种优势。当然,实际业务处理非常复杂,需要结合自己的场景做出合理的选择。

参考

相关推荐
用户2018792831672 小时前
ANR之RenderThread不可中断睡眠state=D
android
煤球王子2 小时前
简单学:Android14中的Bluetooth—PBAP下载
android
小趴菜82272 小时前
安卓接入Max广告源
android
齊家治國平天下2 小时前
Android 14 系统 ANR (Application Not Responding) 深度分析与解决指南
android·anr
ZHANG13HAO2 小时前
Android 13.0 Framework 实现应用通知使用权默认开启的技术指南
android
【ql君】qlexcel2 小时前
Android 安卓RIL介绍
android·安卓·ril
写点啥呢2 小时前
android12解决非CarProperty接口深色模式设置后开机无法保持
android·车机·aosp·深色模式·座舱
IT酷盖2 小时前
Android解决隐藏依赖冲突
android·前端·vue.js
努力学习的小廉3 小时前
初识MYSQL —— 数据库基础
android·数据库·mysql
风起云涌~4 小时前
【Android】浅谈androidx.startup.InitializationProvider
android