概述
Moshi是Square公司在2015年6月開源的有關Json的反序列化及序列化的框架,說到Json,大家應該很快想到Gson,FastJson以及Jackson等著名的開源框架,那為什麼還需要Moshi呢?這個主要是由於Kotlin的緣故,我們知道前面說到的幾大解析庫主要是針對Java解析Json的,當然他們也支援Kotlin,但是Moshi天生對Kotlin友好,而且對Java的解析也毫不遜色,所以不管是在Java跟Kotlin的混編還是在純Kotlin專案中,Moshi表現都很出色。在Java當中,Gson是官方推薦的反序列化及序列化Json框架,同樣在Kotlin中,也有官方的庫kotlinx.serialization,下面簡稱KS,這個庫在kotlinx中,是單獨拿出來了,跟kotlinx.coroutines一樣,接下來我們拿官方庫Gson以及Kotlin的官方庫KS來做個對比,看看彼此的特點。
效能對比
在效能對比之前,我們先簡單對比下這幾種解析框架的解析方式
Method | 支援語言 | 自定義解析 | |
---|---|---|---|
Gson | 反射 | Java/Kotlin | TypeAdapter |
Moshi | 反射/註解 | Java/Kotlin | JsonAdapter |
KS | 編譯外掛 | Kotlin | KSerializer |
通過上表可以看出,Gson跟Moshi都支援反射解析,KS不支援,而且KS只支援Kotlin,三種解析方式都支援自定義解析器,其中在Kotlin解析時,Moshi支援自動生成JsonAdapter,Gson跟KS需要手動編寫,同時的KS可以跨平臺,但是KS對於Gradle的版本要求比較高,需要4.7及以上。
在測試的時候,需要注意幾點
- 用真機:儘量別用模擬器,不同的解析框架在同樣的模擬器上面差距太大,遠超ms級,會給測試帶來誤差
- 最優解:也就是說我們在選擇Json框架測試的時候,一定要選擇該框架的最優解,也就是兼顧開發效率跟解析效率,雖然Gson的TypeAdapter不需要反射,但是它需要手動去編寫程式碼,開發效率較低,所以我們用來對比的是Gson的反射,Moshi的註解以及KS的編譯外掛解析。
我們主要比較兩點:速度跟穩定性
速度
這裡用豆瓣的API進行測試,Api的地址是api.douban.com/v2/movie/to…,這個是返回豆瓣電影評分排名前250的電影,不過這個API做了限流,每次最多返回100條,所以我強求了2次,然後把2次的Json疊加在一起,共200條資料以便於測試,說句題外話,豆瓣在返回的圖片格式全部用了webp,確實很優秀。然後我們就要開始測試了,在測試的時候不管是反序列化還是序列化,我都只測試了一套Json,然後單個框架測試了10次取平均值,注意是在沒有快取位元組碼的情況下,也就是首次解析。原因在於這些開源庫的底層實現都是反射,所以他們會快取位元組碼,導致第二次解析相同的類,速度都超快,因為只需要賦值,當然你可能會說,一套Json的結果是不是不太靠譜,在本次測試中是很靠譜的,首先是我的Json資料量大,而且巢狀層級多,第二是因為他們底層的實現不同,在資料量大的時候這個差異會被放大地很明顯,一會兒看資料大家就知道了。
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 |
小結
由於Moshi底層的IO操作採用的是Okio,所以在序列化的時候效能優於Gson及KS以及其它框架,這個是很好理解的,在反序列化的過程中,我們看到Moshi的解析效率跟Kotlin的官方序列化工具基本持平,但是稍快於Gson,本次測試中沒有把Moshi建立Adapter的時間計算在內,因為他是可以單獨建立作為一個單例,跟解析保持相互獨立,跟前面提到的最優解保持一致。
穩定性
穩定性主要包含兩個方面:預設值和空安全
預設值
我們知道,在Java的解析過程中,如果在Json中缺少某個欄位,我們的Bean物件原有的值保持不變,但是由於Gson無法識別Kotlin的建構函式,導致預設值會失效,下面舉個例子:
@Serializabledata 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
}""")
}複製程式碼
我們解析上述資料,發現Gson解析到的gsonBean物件中的country及hobby這兩個欄位都是null,但是Moshi跟KX反序列化後的物件country跟hobby都是我們給予的預設值,這個問題Gson在解析Java的時候是沒有的,但是在Kotlin中就失效了。原因可以從Gson的原始碼中得到答案,在採用反射解析的時候,Gson構造物件例項時呼叫的是預設無參構造方法,所以沒有預設值也就不足為奇了。那麼hobby為什麼也沒有,因為在Gson並不知道什麼是資料類,所以他依然不認識hobby。
空安全
在Java中,我們可以用註解@Nullable和NotNull來標記一個變數或者方法引數是否可空,但是加註解比較麻煩,所以我們很多時候都不會去加註解,一般都是在使用的時候進行非空判斷,所以Java程式碼在呼叫解析後的Bean物件的時候都需要進行非空判斷,Kotlin在這種情況下進行了完善,可以在定義的時候指定物件是否可空,這樣在使用非空物件的時候就無需進行判斷了,但是如果針對一個方法的引數是非空的,你傳入了一個空值,編譯就會報錯,那麼同樣的道理,如果我們在定義Data類的時候,如果指定了一個欄位為非空型別,那麼如果Json資料裡面這個欄位為Null就應該報錯,下面看看三個框架的實現邏輯
@Serializabledata 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
}""")
}複製程式碼
測試的時候發現Moshi跟KS都報錯了,但是Gson是正常的,按照Kotlin的語法這個是不合理的,我們是需要報錯的,因為age欄位是不可空的,而這裡卻傳了一個空引數,所以Gson在這裡的處理是有問題的。原因我們之前說過,雖然Kotlin最終被編譯成的位元組碼也是執行在JVM上的,但是Gson反射的時候無法區分Java跟Kotlin,所以還是按照Java的解析規則去解析的,因為Json的key為Null在Java中是正常的,即使這在Kotlin中已經無法執行。
結論
針對上面的測試,下面根據專案的實際使用情況總結一下
-
混編專案:使用Moshi,兼顧Java跟Kotlin
-
Java專案:建議使用Gson,如果反序列化需求比較多,建議使用Moshi,因為它內建Okio
-
Kotlin專案:跨平臺的話,使用KS;非跨平臺,如果僅僅是反序列化,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);
//SerializeString 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);
//SerializeString 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);
//SerializeString json = jsonAdapter.fromJson(json);
複製程式碼
Others
- @json:Key轉換
- 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
相對於Java只能通過反射進行解析,針對Kotlin,Moshi提供了兩種解析方式,一種是通過Reflection,一種是通過Codegen本質上是通過註解處理器,你可以採用其中的一種,也可以兩種都使用,下面分別介紹下這兩種解析方式
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()複製程式碼
這種方式會引入Kotlin-Reflect的Jar包,大概有2.5M。
Codegen
上面提到了Reflection,會導致APK體積增大,所以Moshi還提供了另外一種解析方式,就是註解,Moshi的官方叫法叫做Codegen,因為是採用註解生成的,所以除了新增Moshi的Kotlin依賴之外,還需要加上kapt
kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.8.0'複製程式碼
改造Data類
給我們的資料類增加JsonClass註解
@JsonClass(generateAdapter = true)data class ConfigBean( var isGood: Boolean = false, var title: String = "", var type: CustomType = CustomType.DEFAULT)複製程式碼
這樣的話,Moshi會在編譯期生成我們需要的JsonAdapter,然後通過JsonReader遍歷的方式去解析Json資料,這種方式不僅僅不依賴於反射,而且速度快於Kotlin。
這種通過註解生成的Adpter,不需要進行註冊,Moshi會通過註解自動幫我們註冊到Factory裡面,這裡就不貼程式碼了,大家可以去看下官方文件,Read the Fucking Source Code。
高階用法(JsonAdapter)
JsonAdapter是Moshi有別於Gson,FastJson的最大特點,顧名思義,這是一個Json的轉換器,他的主要作用在於將拿到的Json資料轉換成任意你想要的型別,Moshi內建了很多JsonAdapter,有如下這些:
Built-in Type Adapters
- Map:MapJsonAdapter
- Enums:EnumJsonAdapter
- Arrays:ArrayJsonAdapter
- Object:ObjectJsonAdapter
- String:位於StandardJsonAdapters,採用匿名內部類實現
- Primitives (int, float, char,boolean) :基本資料型別的Adapter都在StandardJsonAdapters裡面,採用匿名內部類實現
Custom Type Adapters
對於一些比較簡單規範的資料,使用Moshi內建的JsonAdapter已經完全能夠Cover住,但是由於Json只支援基本資料型別傳輸,所以很多時候不能滿足業務上需要,舉個例子:
{"type": 2,"isGood": 1"title": "TW9zaGkgaXMgZmxleGlibGU="
}複製程式碼
這是一個很普通的Json,包含了5個欄位,我們如果按照服務端返回的欄位來定義解析的Bean,顯然是可以完全解析的,但是我們在實際呼叫的時候,這些資料並不是很乾淨,我們還需要處理一下:
- type:Int型別,我需要Enum,我得定義一個Enum的轉換類,去將Int轉換成Enum
- isGood:Int型別,我需要Boolean,所以我用的時候還得將Int轉成Boolean
- title:String型別,這個欄位是加密過的,可能是通過AES或者RSA加密,這裡我們為了方便測試,只是用Base64對Moshi is flexible對進行encode。
對於客戶端的同學來說,好像沒毛病,以前都是這麼幹的,如果這種不乾淨的Json少點還好,多了之後就很頭疼,每個在用的時候都需要轉一遍,很多時候我這麼幹的時候都覺得浪費時間,而今天有了Moshi之後,我們只需要針對需要轉換的型別定義對應的JsonAdapter,達到一次定義,一勞永逸的效果,Moshi針對常見的資料型別已經定義了Adapter,但是內建的Adapter現在已經不能滿足我們的需求了,所以我們需要自定義JsonAdapter。
實體定義
class ConfigBean {
public CustomType type;
public Boolean isGood;
public String title;
}複製程式碼
此處我們定義的資料型別不是根據伺服器返回的Json資料,而是定義的我們業務需要的格式,那麼最終是通過JsonAdapter轉換器來完成這個轉換,下面開始自定義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
定義一個TypeAdapter繼承自JsonAdapter,傳入對應的泛型,會自動幫我們複寫fromJson跟toJson兩個方法
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;
}
}複製程式碼
至此已經完成Type的轉換,接下來我們再以title舉個例子,別的基本上都是照葫蘆畫瓢,沒什麼難度
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測試
下面我們來測試一下
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
}複製程式碼
符合我們預期的結果,並且我們在開發的時候,只需要將Moshi設定成單例的,一次性將所有的Adapter全部add進去,就可以一勞永逸,然後愉快地進行開發了。
原始碼解析
Moshi底層採用了Okio進行優化,但是上層的JsonReader,JsonWriter等程式碼是直接從Gson借鑑過來的,所以不再過多分析,主要是就Moshi的兩大特性JsonAdapter以及Kotlin的Codegen解析重點分析一下。
Builder
Moshi moshi = new Moshi.Builder().add(new BooleanAdapter()).build();
複製程式碼
Moshi是通過Builder模式進行構建的,支援新增多個JsonAdapter,下面先看看Builder原始碼
public static final class Builder {//儲存所有Adapter的建立方式,如果沒有新增自定義Adapter,則為空final List<
JsonAdapter.Factory>
factories = new ArrayList<
>
();
//新增自定義Adapter,並返回自身public Builder add(Object adapter) {
return add(AdapterMethodsFactory.get(adapter));
}//新增JsonAdapter的建立方法到factories裡,並返回自身public Builder add(JsonAdapter.Factory factory) {
factories.add(factory);
return this;
}//新增JsonAdapter的建立方法集合到factories裡,並返回自身public Builder addAll(List<
JsonAdapter.Factory>
factories) {
this.factories.addAll(factories);
return this;
} //通過Type新增Adapter的建立方法,並返回自身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;
}
});
}//建立一個Moshi的例項public Moshi build() {
return new Moshi(this);
}
}複製程式碼
通過原始碼發現Builder儲存了所有自定義Adapter的建立方式,然後呼叫Builder的build方式建立了一個Moshi的例項,下面看一下Moshi的原始碼。
Moshi
構造方法
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);
}複製程式碼
構造方法裡面建立了factories,然後加入了Builder中的factories,然後又增加了一個BUILT_IN_FACTORIES,我們應該也能猜到這個就是Moshi內建的JsonAdapter,點進去看一下
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這裡面提前用一個靜態程式碼塊加入了所有內建的JsonAdapter
JsonAdapter
JsonAdapter<
ConfigBean>
jsonAdapter = moshi.adapter(ConfigBean.class);
複製程式碼
不管是我們自定義的JsonAdapter還是Moshi內建的JsonAdapter,最終都是為我們的解析服務的,所以最終所有的JsonAdapter最終匯聚成JsonAdapter,我們看看是怎麼生成的,跟一下Moshi的adapter方法,發現最終呼叫的是下面的方法
public <
T>
JsonAdapter<
T>
adapter(Type type, Set<
? extends Annotation>
annotations, @Nullable String fieldName) {
type = canonicalize(type);
// 如果有對應的快取,那麼直接返回快取 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命中了哪一個JsonAdapter,最終通過斷點追蹤,發現是命中了ClassJsonAdapter,既然命中了他,那麼我們就看一下他的具體實現
ClassJsonAdapter
構造方法
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)) {
//建立Moshi跟Filed的繫結關係,便於解析後賦值 createFieldBindings(moshi, t, fields);
} return new ClassJsonAdapter<
>
(classFactory, fields).nullSafe();
}
}複製程式碼
當我們拿到一個JsonAdapter的時候,基本上所有的構建都已經完成,此時可以進行Deserialize 或者Serialize 操作,先看下Deserialize 也就是fromjson方法
JsonReader&
JsonWriter
對於Java的解析,Moshi並沒有在傳輸效率上進行顯著的提升,只是底層的IO操作採用的是Okio,Moshi的創新在於靈活性上面,也就是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那裡直接拷過來的,就是這麼坦誠,不過Moshi也不是全部都是拿來主義,站在Gson 的肩膀上,Moshi的JsonAdapter更加靈活,並且可以採用註解自動生成。
fromjson
ConfigBean cofig = jsonAdapter.fromJson(json);
複製程式碼
這個方法先是呼叫了父類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;
複製程式碼
我們發現fromJson是個過載方法,既可以傳String也可以傳BufferedSource,不過最終呼叫的都是fromJson(JsonReader reader)這個方法,BufferedSource是Okio的一個類,因為Moshi底層的IO採用的是Okio,但是我們發現引數為JsonReader的這個方法是抽象方法,所以具體的實現是是在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();
}
} //遞迴呼叫,直到最後 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一樣,先是呼叫的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,最終呼叫的toJson(JsonWriter writer),然後返回了buffer.readUtf8()。我們繼續看一下子類的具體實現
@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,在編譯期間生成對應的JsonAdapter,我們看一下先加一下註解,看看Kotlin幫我們自動生成的註解跟我們自定義的註解有什麼區別,rebuild一下專案:
CustomType
@JsonClass(generateAdapter = true)data class CustomType(var type: Int, var content: String)複製程式碼
我們來看一下對應生成的JsonAdapter
CustomTypeJsonAdapter
這個類方法很多,我們重點看一下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() //不通過反射,直接建立物件,傳入解析的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的時候要通過Int型別的變數來判斷,而不是通過JsonReader的Name來解析,因為一般拿到一個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() //不通過反射,直接建立物件,傳入解析的Value var result = CustomType(type = type ,content = content ) return result
}//省略toJson複製程式碼
相比於我們自己寫的程式碼,Moshi生成的註解中的程式碼是把Json的key提取出來了,放到一個Options裡面去了,在放的同時也自然生成了一個index,可能這裡不太好理解,為什麼要轉成int呢,這樣的話效率反而不是更低了麼,因為剛開始建立物件的時候需要轉一次,讀取key的時候也要轉一次,這樣還不如直接用String來的快,下面我們跟一下原始碼,看看selectName裡面的具體實現
/** * 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的註釋,我們傳入一個Options,返回一個索引,這個索引也就是我們之前放進去的key的索引,這樣會提高解析效率麼,直觀看起來好像是多此一舉,直接把這個Key的名字給我就好了麼,為什麼還要換成0跟1,可讀性反而貶低了。如果你的key只重複一次,那麼轉不轉成index都是一樣的,因為從二進位制流到string需要一個decode,如果我們解析的是一個列表,那麼同一個key會被decode多次,decode需要時間也需要空間,所以當我們解析無重複的key的時候,換成index跟不換是一樣的,效率差不多,但是當我們解析List的時候,換成Index的時候對於相同的Key我們只需要decode一次,這個在解析列表的時候效率會大大提升。
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,我們發現通過Codegen生成也就是註解的方式,跟反射對比一下,會發現有如下優點:
- 效率高:直接建立物件,無需反射
- APK體積小:無需引入Kotlin-reflect的Jar包
注意事項
在進行kotlin解析的時候不管是採用Reflect還是Codegen,都必須保證型別一致,也就是父類跟子類必須是Java或者kotlin,因為兩種解析方式,最終都是通過ClassType來進行解析的,同時在使用Codegen解析的時候必須保證Koltin的型別是internal
或者public
的。
總結
Moshi整個用法跟原始碼看下來,其實並不是很複雜,但是針對Java跟Kotlin的解析增加了靈活的JsonAdapter,並且在Kotlin中可以自動生成,雖然Gson跟KS也都支援自定義解析,但是賦值需要手動編寫,開發效率較低。不過Moshi也有些缺點,對於Kotlin的Null型別的支援並不友好,這樣會在Kotlin解析的時候如果對於一個不可空的欄位變成了Null就會直接拋異常,感覺不太友好,應該給個預設值或者直接置空比較好一些,還有就是對預設值的支援,如果Json出現了Null型別,那麼解析到對應的欄位依然會被賦值成Null,跟之前的Gson一樣,不過從最新官方的commit已經有人提了issue跟MR,來給非空型別的欄位遇到Json資料對應的Key為Null的時候給予一個預設值,應該會在1.9.0中進行更新,大家可以關注一下。