Moshi 是面向 Android、Java 和 Kotlin 的现代 JSON 库,它可以很容易地将 JSON 解析为 Java 和 Kotlin 类。另外,Moshi 是由 Square 公司所开发,且Moshi 的贡献者也是 Gson 的主要贡献者。
传统 Java Json 库(基于反射)用于 Kotlin 主要产生两个问题:
- 不支持空安全。在 Kotlin 中变量一般是默认为非空的,若 Json 为空则解析出
null
并不会抛出异常,直到数据被使用时才会抛出,这是很诡异的,因为定义为非空的变量是不需要判空的,但实际上是为null
。 - 不支持默认参数。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 异常处理也非常规范,一共只会抛出两种异常:
IOException
:读取 Json 过程中出现 IO 异常,或 Json 格式错误。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
底层其实使用了 JsonReader
与 JsonWriter
进行(反)序列化,这两个类几乎与 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 在 Sex
与 Boolean
之间转换:
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
依次参与,分别是 JsonAdapter
和 SexAdapter
,前者是 Moshi 内置的,后者是我们自定义并添加到 Moshi 实例的。
参考:https://juejin.cn/post/6844904203010179085——晨鹤
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)