新一代Json解析庫(kù)Moshi使用及原理解析

概述

Moshi是Square公司在2015年6月開源的有關(guān)Json的反序列化及序列化的框架,說到Json,大家應(yīng)該很快想到Gson,F(xiàn)astJson以及Jackson等著名的開源框架,那為什么還需要Moshi呢?這個(gè)主要是由于Kotlin的緣故,我們知道前面說到的幾大解析庫(kù)主要是針對(duì)Java解析Json的,當(dāng)然他們也支持Kotlin,但是Moshi天生對(duì)Kotlin友好,而且對(duì)Java的解析也毫不遜色,所以不管是在Java跟Kotlin的混編還是在純Kotlin項(xiàng)目中,Moshi表現(xiàn)都很出色。在Java當(dāng)中,Gson是官方推薦的反序列化及序列化Json框架,同樣在Kotlin中,也有官方的庫(kù)kotlinx.serialization,下面簡(jiǎn)稱KS,這個(gè)庫(kù)在kotlinx中,是單獨(dú)拿出來了,跟kotlinx.coroutines一樣,接下來我們拿官方庫(kù)Gson以及Kotlin的官方庫(kù)KS來做個(gè)對(duì)比,看看彼此的特點(diǎn)。

性能對(duì)比

在性能對(duì)比之前,我們先簡(jiǎn)單對(duì)比下這幾種解析框架的解析方式

Method 支持語言 自定義解析
Gson 反射 Java/Kotlin TypeAdapter
Moshi 反射/注解 Java/Kotlin JsonAdapter
KS 編譯插件 Kotlin KSerializer

通過上表可以看出,Gson跟Moshi都支持反射解析,KS不支持,而且KS只支持Kotlin,三種解析方式都支持自定義解析器,其中在Kotlin解析時(shí),Moshi支持自動(dòng)生成JsonAdapter,Gson跟KS需要手動(dòng)編寫,同時(shí)的KS可以跨平臺(tái),但是KS對(duì)于Gradle的版本要求比較高,需要4.7及以上。

在測(cè)試的時(shí)候,需要注意幾點(diǎn)

  • 用真機(jī):盡量別用模擬器,不同的解析框架在同樣的模擬器上面差距太大,遠(yuǎn)超ms級(jí),會(huì)給測(cè)試帶來誤差
  • 最優(yōu)解:也就是說我們?cè)谶x擇Json框架測(cè)試的時(shí)候,一定要選擇該框架的最優(yōu)解,也就是兼顧開發(fā)效率跟解析效率,雖然Gson的TypeAdapter不需要反射,但是它需要手動(dòng)去編寫代碼,開發(fā)效率較低,所以我們用來對(duì)比的是Gson的反射,Moshi的注解以及KS的編譯插件解析。

我們主要比較兩點(diǎn):速度穩(wěn)定性

速度

這里用豆瓣的API進(jìn)行測(cè)試,Api的地址是https://api.douban.com/v2/movie/top250?start=0&count=100,這個(gè)是返回豆瓣電影評(píng)分排名前250的電影,不過這個(gè)API做了限流,每次最多返回100條,所以我強(qiáng)求了2次,然后把2次的Json疊加在一起,共200條數(shù)據(jù)以便于測(cè)試,說句題外話,豆瓣在返回的圖片格式全部用了webp,確實(shí)很優(yōu)秀。然后我們就要開始測(cè)試了,在測(cè)試的時(shí)候不管是反序列化還是序列化,我都只測(cè)試了一套Json,然后單個(gè)框架測(cè)試了10次取平均值,注意是在沒有緩存字節(jié)碼的情況下,也就是首次解析。原因在于這些開源庫(kù)的底層實(shí)現(xiàn)都是反射,所以他們會(huì)緩存字節(jié)碼,導(dǎo)致第二次解析相同的類,速度都超快,因?yàn)橹恍枰x值,當(dāng)然你可能會(huì)說,一套Json的結(jié)果是不是不太靠譜,在本次測(cè)試中是很靠譜的,首先是我的Json數(shù)據(jù)量大,而且嵌套層級(jí)多,第二是因?yàn)樗麄兊讓拥膶?shí)現(xiàn)不同,在數(shù)據(jù)量大的時(shí)候這個(gè)差異會(huì)被放大地很明顯,一會(huì)兒看數(shù)據(jù)大家就知道了。

Moshi VS Gson(Java)

Test Code

 fun testGsonJava() {
    val json = JsonUtils.getJson("douban.json", this)
    val deserialstart = System.currentTimeMillis()
    val doubanBean = Gson().fromJson(json, DoubanBean::class.java)
    val deserizalend = System.currentTimeMillis()
    val deserialConsume = deserizalend - deserialstart
    val serialstart = System.currentTimeMillis()
    val seriJson = Gson().toJson(doubanBean)
    val serizalend = System.currentTimeMillis()
    val serialConsume = serizalend - serialstart
  }

 fun testMoshiJava() {
    val json = JsonUtils.getJson("douban.json", this)
    val jsonAdapter = Moshi.Builder().build().adapter(DoubanBean::class.java)
    val deserialstart = System.currentTimeMillis()
    val douban = jsonAdapter.fromJson(json)
    val deserizalend = System.currentTimeMillis()
    val deserialConsume = deserizalend - deserialstart
    val serialstart = System.currentTimeMillis()
    val seriJson = jsonAdapter.toJson(douban)
    val serizalend = System.currentTimeMillis()
    val serialConsume = serizalend - serialstart
  }

Test Result

Moshi Gson
Serialization(ms) 24/24/23/23/25 60/60/59/59/60
Deserialization(ms) 66/65/65/65/67 73/79/72/75/74
Moshi VS GSon VS KS(Kotlin)

Test Code

 fun testGsonKotlin() {
    val json = JsonUtils.getJson("douban.json", this)
    val deserialstart = System.currentTimeMillis()
    val doubanBean = Gson().fromJson(json, DoubanBean::class.javaObjectType)
    val deserizalend = System.currentTimeMillis()
    val deserialConsume = deserizalend - deserialstart
    val serialstart = System.currentTimeMillis()
    val seriJson = Gson().toJson(doubanBean)
    val serizalend = System.currentTimeMillis()
    val serialConsume = serizalend - serialstart
  }

  fun testMoshiKotlin() {
    val json = JsonUtils.getJson("douban.json", this)
    val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
    val jsonAdapter = moshi.adapter(DoubanBean::class.java)
    val deserialstart = System.currentTimeMillis()
    val douban = jsonAdapter.fromJson(json)
    val deserizalend = System.currentTimeMillis()
    val deserialConsume = deserizalend - deserialstart
    val serialstart = System.currentTimeMillis()
    val seriJson = jsonAdapter.toJson(douban)
    val serizalend = System.currentTimeMillis()
    val serialConsume = serizalend - serialstart
  }

  fun testKotlinXSerialize() {
    val json = JsonUtils.getJson("douban.json", this)
    val start = System.currentTimeMillis()
    val douban = JSON.parse(DoubanBean.serializer(), json)
    val end = System.currentTimeMillis()
    val consume = end - start
    val serialstart = System.currentTimeMillis()
    val seriJson = JSON.stringify(DoubanBean.serializer(), douban)
    val serizalend = System.currentTimeMillis()
    val serialConsume = serizalend - serialstart
  }

Test Result

Moshi Gson KS
Serialization(ms) 23/27/23/24/27 91/85/85/86/86 38/37/36/43/37
Deserialization(ms) 74/74/73/74/73 93/93/94/89/92 73/72/73/77/71
小結(jié)

由于Moshi底層的IO操作采用的是Okio,所以在序列化的時(shí)候性能優(yōu)于Gson及KS以及其它框架,這個(gè)是很好理解的,在反序列化的過程中,我們看到Moshi的解析效率跟Kotlin的官方序列化工具基本持平,但是稍快于Gson,本次測(cè)試中沒有把Moshi創(chuàng)建Adapter的時(shí)間計(jì)算在內(nèi),因?yàn)樗强梢詥为?dú)創(chuàng)建作為一個(gè)單例,跟解析保持相互獨(dú)立,跟前面提到的最優(yōu)解保持一致。

穩(wěn)定性

穩(wěn)定性主要包含兩個(gè)方面:默認(rèn)值空安全

默認(rèn)值

我們知道,在Java的解析過程中,如果在Json中缺少某個(gè)字段,我們的Bean對(duì)象原有的值保持不變,但是由于Gson無法識(shí)別Kotlin的構(gòu)造函數(shù),導(dǎo)致默認(rèn)值會(huì)失效,下面舉個(gè)例子:

@Serializable
data class Chinese(@Optional val age: Int = 0, @Optional val country: String? = "China") {
  @Optional
  private val hobby: String = "travel"
}

fun main(args: Array<String>) {
  val gsonBean = Gson().fromJson("""{"age":4}""", Chinese::class.javaObjectType)
  val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
  val adapter = moshi.adapter(Chinese::class.java)
  val moshiBean = adapter.fromJson("""{"age":4}""")
  val kxBean = JSON.parse(Chinese.serializer(), """{"age":4}""")

}

我們解析上述數(shù)據(jù),發(fā)現(xiàn)Gson解析到的gsonBean對(duì)象中的country及hobby這兩個(gè)字段都是null,但是Moshi跟KX反序列化后的對(duì)象country跟hobby都是我們給予的默認(rèn)值,這個(gè)問題Gson在解析Java的時(shí)候是沒有的,但是在Kotlin中就失效了。原因可以從Gson的源碼中得到答案,在采用反射解析的時(shí)候,Gson構(gòu)造對(duì)象實(shí)例時(shí)調(diào)用的是默認(rèn)無參構(gòu)造方法,所以沒有默認(rèn)值也就不足為奇了。那么hobby為什么也沒有,因?yàn)樵贕son并不知道什么是數(shù)據(jù)類,所以他依然不認(rèn)識(shí)hobby。

空安全

在Java中,我們可以用注解@Nullable和NotNull來標(biāo)記一個(gè)變量或者方法參數(shù)是否可空,但是加注解比較麻煩,所以我們很多時(shí)候都不會(huì)去加注解,一般都是在使用的時(shí)候進(jìn)行非空判斷,所以Java代碼在調(diào)用解析后的Bean對(duì)象的時(shí)候都需要進(jìn)行非空判斷,Kotlin在這種情況下進(jìn)行了完善,可以在定義的時(shí)候指定對(duì)象是否可空,這樣在使用非空對(duì)象的時(shí)候就無需進(jìn)行判斷了,但是如果針對(duì)一個(gè)方法的參數(shù)是非空的,你傳入了一個(gè)空值,編譯就會(huì)報(bào)錯(cuò),那么同樣的道理,如果我們?cè)诙xData類的時(shí)候,如果指定了一個(gè)字段為非空類型,那么如果Json數(shù)據(jù)里面這個(gè)字段為Null就應(yīng)該報(bào)錯(cuò),下面看看三個(gè)框架的實(shí)現(xiàn)邏輯

@Serializable
data class Chinese(@Optional val age: Int = 0, @Optional val country: String? = "China") {
  @Optional
  private val hobby: String = "travel"
}
fun main(args: Array<String>) {
  val gsonBean = Gson().fromJson("""{"age":null}""", Chinese::class.javaObjectType)
  val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
  val adapter = moshi.adapter(Chinese::class.java)
  val moshiBean = adapter.fromJson("""{"age":null}""")
  val kxBean = JSON.parse(Chinese.serializer(), """{"age":null}""")
}

測(cè)試的時(shí)候發(fā)現(xiàn)Moshi跟KS都報(bào)錯(cuò)了,但是Gson是正常的,按照Kotlin的語法這個(gè)是不合理的,我們是需要報(bào)錯(cuò)的,因?yàn)閍ge字段是不可空的,而這里卻傳了一個(gè)空參數(shù),所以Gson在這里的處理是有問題的。原因我們之前說過,雖然Kotlin最終被編譯成的字節(jié)碼也是運(yùn)行在JVM上的,但是Gson反射的時(shí)候無法區(qū)分Java跟Kotlin,所以還是按照J(rèn)ava的解析規(guī)則去解析的,因?yàn)镴son的key為Null在Java中是正常的,即使這在Kotlin中已經(jīng)無法執(zhí)行。

結(jié)論

針對(duì)上面的測(cè)試,下面根據(jù)項(xiàng)目的實(shí)際使用情況總結(jié)一下

  • 混編項(xiàng)目:使用Moshi,兼顧Java跟Kotlin

  • Java項(xiàng)目:建議使用Gson,如果反序列化需求比較多,建議使用Moshi,因?yàn)樗鼉?nèi)置Okio

  • Kotlin項(xiàng)目:跨平臺(tái)的話,使用KS;非跨平臺(tái),如果僅僅是反序列化,Moshi跟KS均可,如果序列化較多,使用Moshi

基本用法之Java

Dependency

implementation 'com.squareup.moshi:moshi:1.8.0'

Bean

String json = ...;
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<Bean> jsonAdapter = moshi.adapter(Bean.class);
//Deserialize 
Bean bean = jsonAdapter.fromJson(json);
//Serialize
String json = jsonAdapter.toJson(bean);

List

Moshi moshi = new Moshi.Builder().build();
Type listOfCardsType = Types.newParameterizedType(List.class, Bean.class);
JsonAdapter<List<Bean>> jsonAdapter = moshi.adapter(listOfCardsType);
//Deserialize 
List<Bean> beans = jsonAdapter.fromJson(json);
//Serialize
String json = jsonAdapter.fromJson(json);

Map

Moshi moshi = new Moshi.Builder().build();
ParameterizedType newMapType = Types.newParameterizedType(Map.class, String.class, Integer.class);
JsonAdapter<Map<String,Integer>> jsonAdapter = moshi.adapter(newMapType);
//Deserialize 
Map<String,Integer> beans = jsonAdapter.fromJson(json);
//Serialize
String json = jsonAdapter.fromJson(json);

Others

  • @json:Key轉(zhuǎn)換
  • transitent:跳過該字段不解析
public final class Bean {
  @Json(name = "lucky number") int luckyNumber;
  @Json(name = "objec") int data;
  @Json(name = "toatl_price") String totolPrice;
  private transient int total;//jump the field
}

基本用法之Kotlin

相對(duì)于Java只能通過反射進(jìn)行解析,針對(duì)Kotlin,Moshi提供了兩種解析方式,一種是通過Reflection,一種是通過Codegen本質(zhì)上是通過注解處理器,你可以采用其中的一種,也可以兩種都使用,下面分別介紹下這兩種解析方式

Dependency

implementation 'com.squareup.moshi:moshi-kotlin:1.8.0'

Reflection

Data類
data class ConfigBean(
  var isGood: Boolean = false,
  var title: String = "",
  var type: CustomType = CustomType.DEFAULT
)
開始解析
val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .build()

這種方式會(huì)引入Kotlin-Reflect的Jar包,大概有2.5M。

Codegen

上面提到了Reflection,會(huì)導(dǎo)致APK體積增大,所以Moshi還提供了另外一種解析方式,就是注解,Moshi的官方叫法叫做Codegen,因?yàn)槭遣捎米⒔馍傻模猿颂砑覯oshi的Kotlin依賴之外,還需要加上kapt

kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.8.0'
改造Data類

給我們的數(shù)據(jù)類增加JsonClass注解

@JsonClass(generateAdapter = true)
data class ConfigBean(
  var isGood: Boolean = false,
  var title: String = "",
  var type: CustomType = CustomType.DEFAULT
)

這樣的話,Moshi會(huì)在編譯期生成我們需要的JsonAdapter,然后通過JsonReader遍歷的方式去解析Json數(shù)據(jù),這種方式不僅僅不依賴于反射,而且速度快于Kotlin。

這種通過注解生成的Adpter,不需要進(jìn)行注冊(cè),Moshi會(huì)通過注解自動(dòng)幫我們注冊(cè)到Factory里面,這里就不貼代碼了,大家可以去看下官方文檔,Read the Fucking Source Code。

高級(jí)用法(JsonAdapter)

JsonAdapter是Moshi有別于Gson,F(xiàn)astJson的最大特點(diǎn),顧名思義,這是一個(gè)Json的轉(zhuǎn)換器,他的主要作用在于將拿到的Json數(shù)據(jù)轉(zhuǎn)換成任意你想要的類型,Moshi內(nèi)置了很多JsonAdapter,有如下這些:

Built-in Type Adapters

  • Map:MapJsonAdapter
  • Enums:EnumJsonAdapter
  • Arrays:ArrayJsonAdapter
  • Object:ObjectJsonAdapter
  • String:位于StandardJsonAdapters,采用匿名內(nèi)部類實(shí)現(xiàn)
  • Primitives (int, float, char,boolean) :基本數(shù)據(jù)類型的Adapter都在StandardJsonAdapters里面,采用匿名內(nèi)部類實(shí)現(xiàn)

Custom Type Adapters

對(duì)于一些比較簡(jiǎn)單規(guī)范的數(shù)據(jù),使用Moshi內(nèi)置的JsonAdapter已經(jīng)完全能夠Cover住,但是由于Json只支持基本數(shù)據(jù)類型傳輸,所以很多時(shí)候不能滿足業(yè)務(wù)上需要,舉個(gè)例子:

{
"type": 2,
"isGood": 1
"title": "TW9zaGkgaXMgZmxleGlibGU="
}

這是一個(gè)很普通的Json,包含了5個(gè)字段,我們?nèi)绻凑辗?wù)端返回的字段來定義解析的Bean,顯然是可以完全解析的,但是我們?cè)趯?shí)際調(diào)用的時(shí)候,這些數(shù)據(jù)并不是很干凈,我們還需要處理一下:

  • type:Int類型,我需要Enum,我得定義一個(gè)Enum的轉(zhuǎn)換類,去將Int轉(zhuǎn)換成Enum
  • isGood:Int類型,我需要Boolean,所以我用的時(shí)候還得將Int轉(zhuǎn)成Boolean
  • title:String類型,這個(gè)字段是加密過的,可能是通過AES或者RSA加密,這里我們?yōu)榱朔奖銣y(cè)試,只是用Base64對(duì)Moshi is flexible對(duì)進(jìn)行encode。

對(duì)于客戶端的同學(xué)來說,好像沒毛病,以前都是這么干的,如果這種不干凈的Json少點(diǎn)還好,多了之后就很頭疼,每個(gè)在用的時(shí)候都需要轉(zhuǎn)一遍,很多時(shí)候我這么干的時(shí)候都覺得浪費(fèi)時(shí)間,而今天有了Moshi之后,我們只需要針對(duì)需要轉(zhuǎn)換的類型定義對(duì)應(yīng)的JsonAdapter,達(dá)到一次定義,一勞永逸的效果,Moshi針對(duì)常見的數(shù)據(jù)類型已經(jīng)定義了Adapter,但是內(nèi)置的Adapter現(xiàn)在已經(jīng)不能滿足我們的需求了,所以我們需要自定義JsonAdapter。

實(shí)體定義

class ConfigBean {
  public CustomType type;
  public Boolean isGood;
  public String title;
}

此處我們定義的數(shù)據(jù)類型不是根據(jù)服務(wù)器返回的Json數(shù)據(jù),而是定義的我們業(yè)務(wù)需要的格式,那么最終是通過JsonAdapter轉(zhuǎn)換器來完成這個(gè)轉(zhuǎn)換,下面開始自定義JsonAdapter。

Int->Enum

CustomType
enum CustomType {
  DEFAULT(0, "DEFAULT"), BAD(1, "BAD"), NORMAL(2, "NORMAL"), GOOD(3, "NORMAL");
  public int type;
  public String content;
  CustomType(int type, String content) {
    this.type = type;
    this.content = content;
  }
}
TypeAdapter

定義一個(gè)TypeAdapter繼承自JsonAdapter,傳入對(duì)應(yīng)的泛型,會(huì)自動(dòng)幫我們復(fù)寫fromJson跟toJson兩個(gè)方法

public class TypeAdapter  {
  @FromJson
  public CustomType fromJson(int value) throws IOException {
    CustomType type = CustomType.DEFAULT;
    switch (value) {
      case 1:
        type = CustomType.BAD;
        break;
      case 2:
        type = CustomType.NORMAL;
        break;
      case 3:
        type = CustomType.GOOD;
        break;
    }
    return type;
  }
  @ToJson
  public Integer toJson(CustomType value)  {
    return value != null ? value.type : 0;
  }
}

至此已經(jīng)完成Type的轉(zhuǎn)換,接下來我們?cè)僖詔itle舉個(gè)例子,別的基本上都是照葫蘆畫瓢,沒什么難度

StringDecode

TitleAdapter
public class TitleAdapter {
  @FromJson
  public String fromJson(String value) {
    byte[] decode = Base64.getDecoder().decode(value);
    return new String(decode);
  }
  @ToJson
  public String toJson(String value) {
   return new String(Base64.getEncoder().encode(value.getBytes()));
  }
}

Int->Boolean

BooleanAdapter
public class BooleanAdapter {
  @FromJson
  public Boolean fromJson(int value) {
    return value == 1;
  }
  @ToJson
  public Integer toJson(Boolean value) {
    return value ? 1 : 0;
  }
}

Adapter測(cè)試

下面我們來測(cè)試一下

  String json = "{\n" + "\"type\": 2,\n" + "\"isGood\": 1,\n"
      + "\"title\": \"TW9zaGkgaXMgZmxleGlibGU=\"\n"+ "}";
    Moshi moshi = new Moshi.Builder()
        .add(new TypeAdapter())
        .add(new TitleAdapter())
        .add(new BooleanAdapter())
        .build();
    JsonAdapter<ConfigBean> jsonAdapter = moshi.adapter(ConfigBean.class);
    ConfigBean cofig = jsonAdapter.fromJson(json);
    System.out.println("=========Deserialize ========");
    System.out.println(cofig);
    String cofigJson = jsonAdapter.toJson(cofig);
    System.out.println("=========serialize ========");
    System.out.println(cofigJson);

打印Log

=========Deserialize ========
ConfigBean{type=CustomType{type=2, content='NORMAL'}, isGood=true, title='Moshi is flexible'}
=========serialize ========
{"isGood":1,"title":"TW9zaGkgaXMgZmxleGlibGU=","type":2}

符合我們預(yù)期的結(jié)果,并且我們?cè)陂_發(fā)的時(shí)候,只需要將Moshi設(shè)置成單例的,一次性將所有的Adapter全部add進(jìn)去,就可以一勞永逸,然后愉快地進(jìn)行開發(fā)了。

源碼解析

Moshi底層采用了Okio進(jìn)行優(yōu)化,但是上層的JsonReader,JsonWriter等代碼是直接從Gson借鑒過來的,所以不再過多分析,主要是就Moshi的兩大特性JsonAdapter以及Kotlin的Codegen解析重點(diǎn)分析一下。

Builder

   Moshi moshi = new Moshi.Builder().add(new BooleanAdapter()).build();

Moshi是通過Builder模式進(jìn)行構(gòu)建的,支持添加多個(gè)JsonAdapter,下面先看看Builder源碼

public static final class Builder {
//存儲(chǔ)所有Adapter的創(chuàng)建方式,如果沒有添加自定義Adapter,則為空
final List<JsonAdapter.Factory> factories = new ArrayList<>();
//添加自定義Adapter,并返回自身
public Builder add(Object adapter) {
     return add(AdapterMethodsFactory.get(adapter));
    }
//添加JsonAdapter的創(chuàng)建方法到factories里,并返回自身
public Builder add(JsonAdapter.Factory factory) {
      factories.add(factory);
      return this;
    }
//添加JsonAdapter的創(chuàng)建方法集合到factories里,并返回自身
public Builder addAll(List<JsonAdapter.Factory> factories) {
      this.factories.addAll(factories);
      return this;
    }
 //通過Type添加Adapter的創(chuàng)建方法,并返回自身
public <T> Builder add(final Type type, final JsonAdapter<T> jsonAdapter) {
      return add(new JsonAdapter.Factory() {
  @Override 
  public @Nullable JsonAdapter<?> create(
     Type targetType, Set<? extends Annotation> annotations, Moshi moshi) { return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null;
        }
      });
    }
//創(chuàng)建一個(gè)Moshi的實(shí)例
public Moshi build() {
      return new Moshi(this);
    }
  }

通過源碼發(fā)現(xiàn)Builder保存了所有自定義Adapter的創(chuàng)建方式,然后調(diào)用Builder的build方式創(chuàng)建了一個(gè)Moshi的實(shí)例,下面看一下Moshi的源碼。

Moshi

構(gòu)造方法
  Moshi(Builder builder) {
    List<JsonAdapter.Factory> factories = new ArrayList<>(
      builder.factories.size() + BUILT_IN_FACTORIES.size());
    factories.addAll(builder.factories);
    factories.addAll(BUILT_IN_FACTORIES);
    this.factories = Collections.unmodifiableList(factories);
  }

構(gòu)造方法里面創(chuàng)建了factories,然后加入了Builder中的factories,然后又增加了一個(gè)BUILT_IN_FACTORIES,我們應(yīng)該也能猜到這個(gè)就是Moshi內(nèi)置的JsonAdapter,點(diǎn)進(jìn)去看一下

BUILT_IN_FACTORIES
 static final List<JsonAdapter.Factory> BUILT_IN_FACTORIES = new ArrayList<>(5);
  static {
    BUILT_IN_FACTORIES.add(StandardJsonAdapters.FACTORY);
    BUILT_IN_FACTORIES.add(CollectionJsonAdapter.FACTORY);
    BUILT_IN_FACTORIES.add(MapJsonAdapter.FACTORY);
    BUILT_IN_FACTORIES.add(ArrayJsonAdapter.FACTORY);
    BUILT_IN_FACTORIES.add(ClassJsonAdapter.FACTORY);
  }

BUILT_IN_FACTORIES這里面提前用一個(gè)靜態(tài)代碼塊加入了所有內(nèi)置的JsonAdapter

JsonAdapter

JsonAdapter<ConfigBean> jsonAdapter = moshi.adapter(ConfigBean.class);

不管是我們自定義的JsonAdapter還是Moshi內(nèi)置的JsonAdapter,最終都是為我們的解析服務(wù)的,所以最終所有的JsonAdapter最終匯聚成JsonAdapter<ConfigBean>,我們看看是怎么生成的,跟一下Moshi的adapter方法,發(fā)現(xiàn)最終調(diào)用的是下面的方法

public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations,
      @Nullable String fieldName) {
    type = canonicalize(type);
    // 如果有對(duì)應(yīng)的緩存,那么直接返回緩存
    Object cacheKey = cacheKey(type, annotations);
    synchronized (adapterCache) {
      JsonAdapter<?> result = adapterCache.get(cacheKey);
      if (result != null) return (JsonAdapter<T>) result;
    }
  
    boolean success = false;
    JsonAdapter<T> adapterFromCall = lookupChain.push(type, fieldName, cacheKey);
    try {
      if (adapterFromCall != null)
          return adapterFromCall;
      // 遍歷Factories,直到命中泛型T的Adapter
     for (int i = 0, size = factories.size(); i < size; i++) {
 JsonAdapter<T> result = (JsonAdapter<T>) factories.get(i).create(type, annotations, this);
        if (result == null) continue;
        lookupChain.adapterFound(result);
        success = true;
        return result;
      }
    } 
  }

最開始看到這里,我比較奇怪,不太確定我的Config命中了哪一個(gè)JsonAdapter,最終通過斷點(diǎn)追蹤,發(fā)現(xiàn)是命中了ClassJsonAdapter,既然命中了他,那么我們就看一下他的具體實(shí)現(xiàn)

ClassJsonAdapter

構(gòu)造方法

final class ClassJsonAdapter<T> extends JsonAdapter<T> {
  public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
    @Override public @Nullable JsonAdapter<?> create(
        Type type, Set<? extends Annotation> annotations, Moshi moshi) {
        //省略了很多異常判斷代碼
      Class<?> rawType = Types.getRawType(type);
      //獲取Class的所有類型
      ClassFactory<Object> classFactory = ClassFactory.get(rawType);
      Map<String, FieldBinding<?>> fields = new TreeMap<>();
      for (Type t = type; t != Object.class; t = Types.getGenericSuperclass(t)) {
        //創(chuàng)建Moshi跟Filed的綁定關(guān)系,便于解析后賦值
        createFieldBindings(moshi, t, fields);
      }
      return new ClassJsonAdapter<>(classFactory, fields).nullSafe();
    }
}

當(dāng)我們拿到一個(gè)JsonAdapter的時(shí)候,基本上所有的構(gòu)建都已經(jīng)完成,此時(shí)可以進(jìn)行Deserialize 或者Serialize 操作,先看下Deserialize 也就是fromjson方法

JsonReader&JsonWriter

對(duì)于Java的解析,Moshi并沒有在傳輸效率上進(jìn)行顯著的提升,只是底層的IO操作采用的是Okio,Moshi的創(chuàng)新在于靈活性上面,也就是JsonAdapter,而且Moshi的官方文檔上面也提到了

Moshi uses the same streaming and binding mechanisms as Gson. If you’re a Gson user you’ll find Moshi works similarly. If you try Moshi and don’t love it, you can even migrate to Gson without much violence!

所以這里的JsonReader跟JsonWriter說白了都是從Gson那里直接拷過來的,就是這么坦誠(chéng),不過Moshi也不是全部都是拿來主義,站在Gson 的肩膀上,Moshi的JsonAdapter更加靈活,并且可以采用注解自動(dòng)生成。

fromjson
ConfigBean cofig = jsonAdapter.fromJson(json);

這個(gè)方法先是調(diào)用了父類JsonAdapter的fromJson方法

 public abstract  T fromJson(JsonReader reader) throws IOException;
 public final  T fromJson(BufferedSource source) throws IOException {
    return fromJson(JsonReader.of(source));
  }
 public final  T fromJson(String string) throws IOException {
    JsonReader reader = JsonReader.of(new Buffer().writeUtf8(string));
    T result = fromJson(reader);
    return result;
  

我們發(fā)現(xiàn)fromJson是個(gè)重載方法,既可以傳String也可以傳BufferedSource,不過最終調(diào)用的都是fromJson(JsonReader reader)這個(gè)方法,BufferedSource是Okio的一個(gè)類,因?yàn)镸oshi底層的IO采用的是Okio,但是我們發(fā)現(xiàn)參數(shù)為JsonReader的這個(gè)方法是抽象方法,所以具體的實(shí)現(xiàn)是是在ClassJsonAdapter里面,。

 @Override public T fromJson(JsonReader reader) throws IOException {
    T  result = classFactory.newInstance();
    try {
      reader.beginObject();
      while (reader.hasNext()) {
        int index = reader.selectName(options);
        //如果不是Key,直接跳過
        if (index == -1) {
          reader.skipName();
          reader.skipValue();
          continue;
        }
        //解析賦值
        fieldsArray[index].read(reader, result);
      }
      reader.endObject();
      return result;
    } catch (IllegalAccessException e) {
      throw new AssertionError();
    }
  }
  
//遞歸調(diào)用,直到最后 
void read(JsonReader reader, Object value) throws IOException, IllegalAccessException {
      T fieldValue = adapter.fromJson(reader);
      field.set(value, fieldValue);
    }
toJson
String cofigJson = jsonAdapter.toJson(cofig);

跟fromJson一樣,先是調(diào)用的JsonAdapter的toJson方法

 public abstract void toJson(JsonWriter writer,  T value) throws IOException;
 public final void toJson(BufferedSink sink, T value) throws IOException {
    JsonWriter writer = JsonWriter.of(sink);
    toJson(writer, value);
  }
 public final String toJson( T value) {
    Buffer buffer = new Buffer();
    try {
      toJson(buffer, value);
    } catch (IOException e) {
      throw new AssertionError(e); // No I/O writing to a Buffer.
    }
    return buffer.readUtf8();
  }

不管傳入的是泛型T還是BufferedSink,最終調(diào)用的toJson(JsonWriter writer),然后返回了buffer.readUtf8()。我們繼續(xù)看一下子類的具體實(shí)現(xiàn)


  @Override public void toJson(JsonWriter writer, T value) throws IOException {
    try {
      writer.beginObject();
      for (FieldBinding<?> fieldBinding : fieldsArray) {
        writer.name(fieldBinding.name);
        //將fieldsArray的值依次寫入writer里面
        fieldBinding.write(writer, value);
      }
      writer.endObject();
    } catch (IllegalAccessException e) {
      throw new AssertionError();
    }
  }

Codegen

Moshi’s Kotlin codegen support is an annotation processor. It generates a small and fast adapter for each of your Kotlin classes at compile time. Enable it by annotating each class that you want to encode as JSON:

所謂Codegen,也就是我們上文提到的Annotation,在編譯期間生成對(duì)應(yīng)的JsonAdapter,我們看一下先加一下注解,看看Kotlin幫我們自動(dòng)生成的注解跟我們自定義的注解有什么區(qū)別,rebuild一下項(xiàng)目:

CustomType

@JsonClass(generateAdapter = true)
data class CustomType(var type: Int, var content: String)

我們來看一下對(duì)應(yīng)生成的JsonAdapter

CustomTypeJsonAdapter

這個(gè)類方法很多,我們重點(diǎn)看一下formJson跟toJson

override fun fromJson(reader: JsonReader): CustomType {
 private val options: JsonReader.Options = JsonReader.Options.of("type", "content", "age")
        var type: Int? = null
        var content: String? = null
        reader.beginObject()
        while (reader.hasNext()) {
            when (reader.selectName(options)) {
            //按照變量的定義順序依次賦值
                0 -> type = intAdapter.fromJson(reader) 
                1 -> content = stringAdapter.fromJson(reader) 
                -1 -> {
                    reader.skipName()
                    reader.skipValue()
                }
            }
        }
        reader.endObject()
        //不通過反射,直接創(chuàng)建對(duì)象,傳入解析的Value
        var result = CustomType(type = type ,content = content )
        return result
    }

    override fun toJson(writer: JsonWriter, value: CustomType?) {
        writer.beginObject()
        writer.name("type")//寫入type
        intAdapter.toJson(writer, value.type)
        writer.name("content")//寫入content
        stringAdapter.toJson(writer, value.content)
        writer.endObject()
    }

在看這段代碼之前,我開始很奇怪Moshi為什么在遍歷JsonReader的時(shí)候要通過Int類型的變量來判斷,而不是通過JsonReader的Name來解析,因?yàn)橐话隳玫揭粋€(gè)JsonReader之后,我們都是下面這種寫法:

 override fun fromJson(reader: JsonReader): CustomType {
        var type: Int? = null
        var content: String? = null
        reader.beginObject()
        while (reader.hasNext()) {
            when (reader.nextName()) {
            //按照變量的定義順序依次賦值
                "type" -> type = reader.nextInt()
                "content" -> content = reader.nextString() 
                else -> {
                    reader.skipValue()
                }
            }
        }
        reader.endObject()
        //不通過反射,直接創(chuàng)建對(duì)象,傳入解析的Value
        var result = CustomType(type = type ,content = content )
        return result
    }
//省略toJson

相比于我們自己寫的代碼,Moshi生成的注解中的代碼是把Json的key提取出來了,放到一個(gè)Options里面去了,在放的同時(shí)也自然生成了一個(gè)index,可能這里不太好理解,為什么要轉(zhuǎn)成int呢,這樣的話效率反而不是更低了么,因?yàn)閯傞_始創(chuàng)建對(duì)象的時(shí)候需要轉(zhuǎn)一次,讀取key的時(shí)候也要轉(zhuǎn)一次,這樣還不如直接用String來的快,下面我們跟一下源碼,看看selectName里面的具體實(shí)現(xiàn)

 /**
   * If the next token is a {@linkplain Token#NAME property name} that's in {@code options}, this
   * consumes it and returns its index. Otherwise this returns -1 and no name is consumed.
   */
@CheckReturnValue 
public abstract int selectName(Options options) throws IOException;

通過注釋我們可以看到selectName的注釋,我們傳入一個(gè)Options,返回一個(gè)索引,這個(gè)索引也就是我們之前放進(jìn)去的key的索引,這樣會(huì)提高解析效率么,直觀看起來好像是多此一舉,直接把這個(gè)Key的名字給我就好了么,為什么還要換成0跟1,可讀性反而貶低了。如果你的key只重復(fù)一次,那么轉(zhuǎn)不轉(zhuǎn)成index都是一樣的,因?yàn)閺亩M(jìn)制流到string需要一個(gè)decode,如果我們解析的是一個(gè)列表,那么同一個(gè)key會(huì)被decode多次,decode需要時(shí)間也需要空間,所以當(dāng)我們解析無重復(fù)的key的時(shí)候,換成index跟不換是一樣的,效率差不多,但是當(dāng)我們解析List的時(shí)候,換成Index的時(shí)候?qū)τ谙嗤腒ey我們只需要decode一次,這個(gè)在解析列表的時(shí)候效率會(huì)大大提升。

ConfigBean

@JsonClass(generateAdapter = true)
data class ConfigBean(var isGood: Boolean ,var title: String ,var type: CustomType)

ConfigBeanJsonAdapter

override fun fromJson(reader: JsonReader): ConfigBean {
    var isGood: Boolean? = null
    var title: String? = null
    var type: CustomType? = null
    reader.beginObject()
    while (reader.hasNext()) {
        when (reader.selectName(options)) {
            0 -> isGood = booleanAdapter.fromJson(reader) 
            1 -> title = stringAdapter.fromJson(reader) 
            2 -> type = customTypeAdapter.fromJson(reader)
            -1 -> {
                reader.skipName()
                reader.skipValue()
            }
        }
    }
    reader.endObject()
    var result = ConfigBean(isGood = isGood ,title = title ,type = type
    return result
}

override fun toJson(writer: JsonWriter, value: ConfigBean?) {
    writer.beginObject()
    writer.name("isGood")
    booleanAdapter.toJson(writer, value.isGood)
    writer.name("title")
    stringAdapter.toJson(writer, value.title)
    writer.name("type")
    customTypeAdapter.toJson(writer, value.type)
    writer.endObject()
}

通過查看生成的CustomTypeJsonAdapter以及ConfigBeanJsonAdapter,我們發(fā)現(xiàn)通過Codegen生成也就是注解的方式,跟反射對(duì)比一下,會(huì)發(fā)現(xiàn)有如下優(yōu)點(diǎn):

  • 效率高:直接創(chuàng)建對(duì)象,無需反射
  • APK體積小:無需引入Kotlin-reflect的Jar包

注意事項(xiàng)

在進(jìn)行kotlin解析的時(shí)候不管是采用Reflect還是Codegen,都必須保證類型一致,也就是父類跟子類必須是Java或者kotlin,因?yàn)閮煞N解析方式,最終都是通過ClassType來進(jìn)行解析的,同時(shí)在使用Codegen解析的時(shí)候必須保證Koltin的類型是internal或者public的。

總結(jié)

Moshi整個(gè)用法跟源碼看下來,其實(shí)并不是很復(fù)雜,但是針對(duì)Java跟Kotlin的解析增加了靈活的JsonAdapter,并且在Kotlin中可以自動(dòng)生成,雖然Gson跟KS也都支持自定義解析,但是賦值需要手動(dòng)編寫,開發(fā)效率較低。不過Moshi也有些缺點(diǎn),對(duì)于Kotlin的Null類型的支持并不友好,這樣會(huì)在Kotlin解析的時(shí)候如果對(duì)于一個(gè)不可空的字段變成了Null就會(huì)直接拋異常,感覺不太友好,應(yīng)該給個(gè)默認(rèn)值或者直接置空比較好一些,還有就是對(duì)默認(rèn)值的支持,如果Json出現(xiàn)了Null類型,那么解析到對(duì)應(yīng)的字段依然會(huì)被賦值成Null,跟之前的Gson一樣,不過從最新官方的commit已經(jīng)有人提了issue跟MR,來給非空類型的字段遇到Json數(shù)據(jù)對(duì)應(yīng)的Key為Null的時(shí)候給予一個(gè)默認(rèn)值,應(yīng)該會(huì)在1.9.0中進(jìn)行更新,大家可以關(guān)注一下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容