Android(Java) 平臺已經有許多 Json 庫了,包括 Google 推薦的 Gson,廣受歡迎的 Jackson,阿里的 FastJson 等,但今天要說的是大名鼎鼎 Square 公司的 Moshi. (什麼?沒據說過 Square?OkHttp
, Retrofit
可都是他的著做)java
輪子已經有不少了,不過自從 Google 將 Kotlin 定爲 Android 親兒子開發語言後,居然包括 Gson 在內的幾乎全部 Json 庫均不支持,這固然能夠理解,由於他們大多數採用了 Java 反射機制,註定難以適配 Kotlin 獨有的特性。所以今天要介紹 Moshi
—— 一個與 Kotlin 兼容極好的現代化解析庫。android
傳統 Java Json 庫用於 Kotlin 主要產生兩個問題:git
null
而不會拋出異常,直到數據使用時才拋出詭異的 NPE. 爲啥說詭異?由於定義非空的變量默認不須要進行空判斷,但實際上他是空的,有點掛羊頭買狗肉的意思。data class
極大地方便了開發,默認參數的語法更是直呼太爽。惋惜這種現代化寫法或遇到兩個問題:①默認參數無效。②解析失敗由於沒有無參構造函數。而解決辦法更是使人崩潰:①不要使用默認參數。②給全部形參所有加上默認參數。所以,若是你已經使用 Kotlin 做爲主要語言,Moshi 將會是絕佳的選擇。(KT 自帶的解析庫也能夠考慮)github
另外 Moshi 的貢獻者也是 Gson 的主要貢獻者,由於 Gson 的先天不足且他已經離開了 Google,故開發了 Moshi,具體能夠參考他在 Reddit 的回答:Why use Moshi over Gson?json
在 Kotlin 中使用 Moshi 有兩個方案:①使用 Kotlin 反射。②使用註解生成。由於 Kotlin 反射庫高達 2MB,通常咱們採用第二個方案,並且理論上它也比反射效率更高一些,由於大多數工做在編譯器就作完了。數組
Moshi 使用一個叫 Adapter
的東西負責序列化與反序列化,每一個 Kotlin 類都對應一個 Adapter,命名規則爲 數據類名+JsonAdapter
,藉助於內置的基本數據類型 Adapter(Int, String 等) 從而實現任意類的解析。顯然,咱們須要在 Adapter 內部列出每一個字段並定義解析方法,這是模板化的重複性工做,能夠利用註解幫助實現。安全
上面一段看不懂也不要緊,只須要記得在須要序列化(反序列化)的類上加上 @JsonClass(generateAdapter = true)
註解就好了,例如:bash
@JsonClass(generateAdapter = true)
data class Person(
val name: String,
val age: Int,
val sex: Boolean
)
複製代碼
補充一下 Adapter 以便理解ide
傳統的 Json 庫中,每當讀取到一個 Json 屬性能夠利用反射找到 class 中對應的字段(變量)進行賦值。如今咱們拋棄了反射,那麼就須要一個手段找到這個變量,解決方案很是粗暴,那就是在編譯時就把已知變量所有列出來,沒有列出來的就忽略。負責執行這個工做的就是 Adapter.函數
爲何不用反射?
①若使用 Java 反射那麼沒法支持空安全等 Kotlin 特性。②若使用 Kotlin 反射則須要引入一個 2MB 大小的 jar 文件。
以後的使用相似 Gson 很是簡單,相比 FastJson 麻煩一丟丟,畢竟要先建立實例。
val json = "..."
val moshi: Moshi = Moshi.Builder().build()
val jsonAdapter: JsonAdapter<Person> = moshi.adapter(Person::class.java)
person: Person = jsonAdapter.fromJson(json)
System.out.println(person)
複製代碼
實際使用中能夠將 Moshi 做爲單例。
Moshi 異常處理也很是規範,它一共只會拋出兩種異常:
IOException
:讀取 Json 過程當中出現 IO 異常,或者 Json 格式錯誤。JsonDataException
:數據類型不匹配。若是要解析成集合,須要先用 Types.newParameterizedType()
包裝一下:
val personArrayJson: String = "..."
val type: Type = Types.newParameterizedType(List::class.java, Person::class.java)
val adapter: JsonAdapter<List<Person>> = moshi.adapter(type)
val persons: List<Person> = adapter.fromJson(personArrayJson)
複製代碼
Adapter 底層其實使用了 JsonReader
與 JsonWriter
進行(反)序列化,這兩個類幾乎是從 Gson 抄過來的,API 很是相似,官方原話: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,下面是一個解析 Person 數組的例子
val reader = JsonReader.of(Okio.buffer(Okio.source(jsonFile)))
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 不得不進行 UTF8 解碼並分配內存。相比之下咱們能夠事先準備好有可能出現的字段名,而後直接進行二進制比對,並返回在字段名序列中的下標。
舉個栗子🌰:批改做業時每次都把每道題算一遍再比對答案是很低效的,由於咱們已經知道了有哪些題目,不妨先算出正確答案而後直接對比便可。
首先使用 JsonReader.Options.of()
建立字段名稱數組,而後使用 reader.selectName()
讀取並匹配字段名,優化後代碼以下:
val names = JsonReader.Options.of("name", "age", "sex")
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
}
複製代碼
更多時候,Json 數據格式是已知的,但其值的格式與 Kotlin Class 定義不一樣,此時若徹底使用流式 API 解析就太麻煩了,自定義 Adapter 應運而生,經過 Adapter 咱們能夠控制 Json 與 class 如何轉換。
有趣的是,Adapter 就是一個普通的類,習慣上咱們給類名加上 Adapter
後綴以示區分,但實際上它並不繼承自任何父類,也無需實現任何接口。只須要定義兩個函數分別用於 Json→Class 與 Class→Json 的轉換,並分別加上 @FromJson
與 @ToJson
註解就好了。
爲了演示首先改變一下 Person 的定義:
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: Boolean): 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: Person = moshi.adapter(Person::class.java).fromJson(json)
複製代碼
確定有小夥伴好奇,爲何不直接繼承某個類或實現某個接口,恰恰用註解的方式定義函數呢?
這一設計一開始確實使人困擾,包括這些函數參數應該填什麼、返回值應該是是什麼都沒有準確的文檔。我第一次運行的感受就是「這竟然跑的通?」「它爲何不崩潰呢?」🤣 而這些看似繁雜的設計正是 Moshi Adapter 的靈活性精髓所在。
事實上,@FromJson
與 @ToJson
所註釋的函數的參數類型或返回值類型是任意的,只要它能被 Moshi 識別便可! 換句話說你能夠把 Adapter 當成一箇中間步驟,許多 Adapter 組成處理鏈將數據一步步轉化成所需的類型。以上面的 Demo 爲例🌰,咱們接受一個 Boolean
類型的參數並返回 Sex
(也就是最終所需的類型),那麼整個反序列化過程其實有兩個 Adapter 依次參與,分別是 JsonAdapter<Boolean>
和 SexAdapter
,前者是 Moshi 內置的,後者是咱們自定義並添加到 Moshi 實例的。
假如 Json 源使用 Int 表示 Boolean,那麼咱們能夠再寫一個 BooleanAdapter
,接受 Int
類型參數返回 Boolean
,而後再經過 SexAdapter
最終獲得 Sex
.
固然,你也能夠直接在
SexAdapter
中接受Int
參數並返回Sex
. 可是多一個 Adapter 的優勢是若還有其餘場合須要用到Boolean
,就能夠複用這段邏輯了。
What's more! 這還僅僅是 Adapter 的第一類用法,下面還有更喪心病狂的函數簽名供食用😝。
假設咱們有這樣一個 Json 定義:
[
{
"type": "person",
"data": {
"name": "Bob",
"age": 23,
"sex": true
}
},
{
"type": "job",
"data": {
"name": "developer",
"salary": 20000
}
}
]
複製代碼
這是一個數組,每一項都有 type
和 data
兩個字段,討厭的是根據 type
的不一樣,data
的類型也不一樣。對於這種「髒」數據咱們能夠考慮使用流式 API 手動解析,但若 data
類型很複雜或不少怎麼辦?
咱們想,能不能寫一個 Adapter 先判斷 type
的值,根據值的不一樣再選用不一樣的 Adapter 進一步解析,而這些 Adapter 就能夠根據 data class 經過 @JsonClass(generateAdapter = true)
註解自動生成了。
這個需求經過已有方式很難解決,它的核心是 「目標類型是半已知的」。
先來看一下 Adapter 中函數的具體簽名要求:
<any access modifier> R fromJson(T value) throws <any>;
<any access modifier> R fromJson(JsonReader jsonReader) throws <any>;
<any access modifier> R fromJson(JsonReader jsonReader, JsonAdapter<any> delegate, <any more delegates>) throws <any>;
複製代碼
<any access modifier> R toJson(T value) throws <any>;
<any access modifier> void toJson(JsonWriter writer, T value) throws <any>;
<any access modifier> void toJson(JsonWriter writer, T value, JsonAdapter<any> delegate, <any more delegates>) throws <any>;
複製代碼
上面咱們一直使用的是第一個函數簽名,即:接受一個任意類型的(已經支持轉換的)參數並返回一個任意類型。如今咱們要使用第三類,即:接受一個 JsonReader(用於流式解析)以及一系列可能用到的 Adapter(用於委託),而後返回一個任意類型。
首先定義一下數據類:
sealed class Item {
abstract val name: String
}
@JsonClass(generateAdapter = true)
data class Person(
override val name: String,
val age: Int,
val sex: Boolean
) : Item()
@JsonClass(generateAdapter = true)
data class Job(
override val name: String,
val salary: Int
) : Item()
複製代碼
而後就能夠寫出 Adapter:
class ItemAdapter {
private val names = JsonReader.Options.of("type", "data")
@FromJson
fun fromJson( reader: JsonReader, person: JsonAdapter<Person>, job: JsonAdapter<Job> ): Item? {
reader.beginObject()
// 解析 type 字段
// peek() 用於得到一個新的 JsonReader 以便重複解析。
// 由於咱們沒法肯定 type 和 data 哪個會先讀取到,所以須要利用 peek 先單獨取出 type,
// 而後再開始正式讀取。
var type: String? = null
val peek = reader.peekJson()
loop@ while (peek.hasNext()) {
when (peek.selectName(names)) {
0 -> {
type = peek.nextString()
break@loop // 只要找到 type 就好了
}
1 -> peek.skipValue()
else -> {
peek.skipName()
peek.skipValue()
}
}
}
// 真正開始解析數據
var item: Item? = null
while (reader.hasNext()) {
when (reader.selectName(names)) {
0 -> reader.skipValue()
1 -> when (type) {
"person" -> item = person.fromJson(reader)
"job" -> item = job.fromJson(reader)
else -> reader.skipValue() // 未知 type,跳過
}
else -> {
reader.skipName()
reader.skipValue()
}
}
}
reader.endObject()
return item
}
@ToJson
fun toJson( writer: JsonWriter, value: Item, person: JsonAdapter<Person>, job: JsonAdapter<Job> ) {
// begin(end)Object 不能夠放在 when 外面,不然若遇到不支持的類型會解析出空對象「{}」
when (value) {
is Person -> writer.writeItem("person", person, value)
is Job -> writer.writeItem("job", job, value)
}
}
private fun <T> JsonWriter.writeItem(type: String, adapter: JsonAdapter<T>, data: T) {
beginObject()
name("type")
value(type)
name("data")
adapter.toJson(this, data)
endObject()
}
}
複製代碼
在這個例子中咱們首先經過 JsonReader
手動解析出了 type
字段,而後根據具體的值將後續解析委託給了自動生成的 Adapter. 須要注意的一點是在流式 API 中字段解析的順序依賴於 Json 字符串,所以有可能先解析出 data
,在這種狀況下因爲缺乏 type
咱們是沒法處理這個數據的(不知道委託給誰)。所以咱們要先取出 type
並忽略其餘一切字段,可是 Reader 的讀取是單向的,若是忽略了先解析出的 data
後續就無法再次取得。因而使用 reader.peekJson()
方法取得一個臨時 Reader,它的讀取不會影響原先 Reader.
序列化同理,判斷具體的數據類型後委託給對應的 Adapter 就好了。
在反序列化時,也許有同窗想先臨時保存一下
data
的原始數據,在獲得type
後再進行解析,從而避免使用peekJson()
。問題在於 Moshi 是沒有中間層的(例如 Gson 中的JsonElement
),要麼解析成一個具體的數據類型,要麼忽略,因此這個想法不可行。