Json 解析库 Moshi 的介绍与使用

Json 解析库 Moshi 的介绍与使用,第1张

Moshi

Moshi 是面向 Android、Java 和 Kotlin 的现代 JSON 库,它可以很容易地将 JSON 解析为 Java 和 Kotlin 类。另外,Moshi 是由 Square 公司所开发,且Moshi 的贡献者也是 Gson 的主要贡献者。

传统 Java Json 库(基于反射)用于 Kotlin 主要产生两个问题:

  1. 不支持空安全。在 Kotlin 中变量一般是默认为非空的,若 Json 为空则解析出 null 并不会抛出异常,直到数据被使用时才会抛出,这是很诡异的,因为定义为非空的变量是不需要判空的,但实际上是为 null
  2. 不支持默认参数。Kotlin 的 data class 和默认参数的语法极大地方便了开发,但是在使用传统 Json 解析库时往往会遇到两个问题:
    • 默认参数失效
    • 解析失败(因为没有无参构造方法)

若使用 Moshi,则不会出现这些问题。

因此,如果项目使用 Kotlin 进行开发,Moshi 无疑是最好的选择。

基础使用

Dependency

implementation("com.squareup.moshi:moshi-kotlin:1.13.0")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.13.0")

在 Kotlin 中使用 Moshi 有两个方案:

  • 使用 Kotlin 反射
  • 使用注解生成

因为 Kotlin 的反射库将近 2MB,所以一般采用注解生成,而且理论上会比反射效率更高,因为大多数工作在编译器进行编译时就完成了。

Moshi 使用 Adapter 类负责序列化和反序列化,每个 Kotlin 类对应一个 Adapter,命名规则为 数据类名 + JsonAdapter,借助内置的基本数据类型从而实现任意类的解析(即使不是基本数据类型,也可以进行转换),这里可以利用注解来实现。

PS:传统的 Json 库中,每当读取到一个 Json 属性可以利用反射找到 Class
中对应的属性字段进行赋值。现在我们不再使用反射,那么怎么找到是哪个变量呢?那就是在编译时把已知变量全部列出来,没有列出来的变量就忽略,负责这个工作的就是
Adapter。

只需要在需要序列化(反序列化)的类上加上 @JsonClass(generateAdapter = true) 注解就行了。

@JsonClass(generateAdapter)
data class Person(
	val name: String,
	val age: Int,
	val sex: Boolean
)

之后的使用与 Gson 类似。

Bean

val json = "..."

val moshi: Moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(Person::class.java)

val person: Person = jsonAdapter.fromJson(json)

实际使用中可以将 Moshi 作为单例,也可通过 Hilt 提供,此处不展开。

Moshi 异常处理也非常规范,一共只会抛出两种异常:

  1. IOException:读取 Json 过程中出现 IO 异常,或 Json 格式错误。
  2. JsonDataException:数据类型不匹配。

List

如果是要解析成集合,需要先用 Types.newParameterizedType() 包装一下:

val personArrayJson = "..."

val type: Type = Types.newParameterizedType(List::class.java, Person::class.java)
val adapter: JsonAdapter<List<Person>> = moshi.adapter(type)

val persons = adapter.fromJson(personArrayJson)

Map

解析 Map 与 List 类似。

val json = "..."

val type = Types.newParameteriedType(Map::class.java, String::class.java, Int::class.java)
val adapter = moshi.adapter(type)

val map: Map<String, Int> = adapter.fromJson(json)

流式手动解析

Adapter 底层其实使用了 JsonReaderJsonWriter 进行(反)序列化,这两个类几乎与 Gson 中一样。(Moshi uses the same streaming and binding mechanisms as Gson. If you’re a Gson user you’ll find Moshi works similarly.)

对于动态(脏)的 Json 数据,我们难以预先得知其包含的字段,此时就可以采用流式解析。Moshi 使用 Okio 作为底层,我们需要通过 Okio 创建数据源来创建 JsonReader

val json = "..."

val reader = JsonReader.of(Okio.buffer(Okio.source(json)))

fun readPersonArray(reader: JsonReader): List<Person> {
	val list = mutableListOf<Person>()
	reader.beginArray()
	while(reader.hasNext()) {
		var name = ""
		var age = 0
		var sex = true
		reader.beginObject()
		while(reader.hasNext()) {
			when(reader.nextName()) {
				"name" -> name = reader.nextString()
				"age" -> age = reader.nextInt()
				"sex" -> sex = reader.nextBoolean()
				else -> reader.skipValue()
			}
		}
		reader.endObject()
		val person = Person(name, age, sex)
		list.add(person)
	}
	reader.endArray()
	return list
}

使用 selectName 优化

解析数组时,一些字段名会重复重现,此时 Moshi 不得不进行 UTF-8 解码并分配内存,我们可以事先准备好有可能出现的字段名,直接进行二进制化比对,并返回字段名序列中的下标。

首先使用 JsonReader.Options.of() 创建字段名称数组,然后使用 reader.selectName() 读取并匹配字段名。

val json = "..."

val names = JsonReader.Options.of("name", "age", "sex")
val reader = JsonReader.of(Okio.buffer(Okio.source(json)))

fun readPersonArray(reader: JsonReader): List<Person> {
	val list = mutableListOf<Person>()
	reader.beginArray()
	while(reader.hasNext()) {
		var name = ""
		var age = 0
		var sex = true
		reader.beginObject()
		while(reader.hasNext()) {
			when(reader.selectName(names)) {
				0 -> name = reader.nextString()
				1 -> age = reader.nextInt()
				2 -> sex = reader.nextBoolean()
				else -> {
					reader.skipName()
					reader.skipValue()
				}
			}
		}
		reader.endObject()
		val person = Person(name, age, sex)
		list.add(person)
	}
	reader.endArray()
	return list
}

自定义 Adapter

更多时候,Json 数据格式是已知的,但其值的格式与 Kotlin Class 定义不同,此时若完全使用流式 API 解析就太麻烦了,此时可以自定义 Adaper,通过 Adapter 控制 Json 与 Class 如何转换。

Adapter 就是一个普通的类,习惯给类名加上 Adapter 后缀以示区分,但实际上它并不继承自任何父类,也无需实现任何接口,只需要定义两个函数分别用于 Json -> Class 与 Class -> Json 的转换,并分别加上 @FromJson@ToJson 注解就行了。

@JsonClass(generateAdapter)
data class Person(
	val name: String,
	val age: Int,
	val sex: Sex // 将性别改为枚举
)

enum class Sex {
    MALE, FEMALE
}

现在 Sex 不再是基础数据类型 Moshi 无法识别,创建一个 SexAdapter 帮助 Moshi 在 SexBoolean 之间转换:

class SexAdapter {
	@FromJson
	fun fromJson(value: String): Sex {
		return if(value) Sex.MALE else Sex.FEMALE
	}
	
	@ToJson
	fun toJson(sex: Sex): Boolean {
		return sex == Sex.MALE
	}
}

最后进行注册即可成功解析。

val json = "..."
val moshi = Moshi.Builder().add(SexAdapter()).build()
val person = moshi.adapter(Person::class.java).fromJson(json)

事实上,@FromJson@ToJson 所注释的函数的参数类型或返回值类型是任意的,只要它能被 Moshi 识别即可。 换句话说你可以把 Adapter 当成一个中间步骤,许多 Adapter 组成处理链将数据一步步转化成所需的类型。以上面的 Demo 为例,我们接受一个 Boolean 类型的参数并返回 Sex(也就是最终所需的类型),那么整个反序列化过程其实有两个 Adapter 依次参与,分别是 JsonAdapterSexAdapter,前者是 Moshi 内置的,后者是我们自定义并添加到 Moshi 实例的。


参考:https://juejin.cn/post/6844904203010179085——晨鹤

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/721240.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-25
下一篇 2022-04-25

发表评论

登录后才能评论

评论列表(0条)

保存