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來作個對比,看看彼此的特色。java
在性能對比以前,咱們先簡單對比下這幾種解析框架的解析方式git
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及以上。github
在測試的時候,須要注意幾點web
咱們主要比較兩點:速度跟穩定性json
這裏用豆瓣的API進行測試,Api的地址是api.douban.com/v2/movie/to…,這個是返回豆瓣電影評分排名前250的電影,不過這個API作了限流,每次最多返回100條,因此我強求了2次,而後把2次的Json疊加在一塊兒,共200條數據以便於測試,說句題外話,豆瓣在返回的圖片格式所有用了webp,確實很優秀。而後咱們就要開始測試了,在測試的時候無論是反序列化仍是序列化,我都只測試了一套Json,而後單個框架測試了10次取平均值,注意是在沒有緩存字節碼的狀況下,也就是首次解析。緣由在於這些開源庫的底層實現都是反射,因此他們會緩存字節碼,致使第二次解析相同的類,速度都超快,由於只須要賦值,固然你可能會說,一套Json的結果是否是不太靠譜,在本次測試中是很靠譜的,首先是個人Json數據量大,並且嵌套層級多,第二是由於他們底層的實現不一樣,在數據量大的時候這個差別會被放大地很明顯,一下子看數據你們就知道了。api
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 |
Test Codebash
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的構造函數,致使默認值會失效,下面舉個例子:
@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}""")
}
複製代碼
咱們解析上述數據,發現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就應該報錯,下面看看三個框架的實現邏輯
@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}""")
}
複製代碼
測試的時候發現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
implementation 'com.squareup.moshi:moshi:1.8.0'
複製代碼
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);
複製代碼
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);
複製代碼
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);
複製代碼
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
}
複製代碼
相對於Java只能經過反射進行解析,針對Kotlin,Moshi提供了兩種解析方式,一種是經過Reflection,一種是經過Codegen本質上是經過註解處理器,你能夠採用其中的一種,也能夠兩種都使用,下面分別介紹下這兩種解析方式
implementation 'com.squareup.moshi:moshi-kotlin:1.8.0'
複製代碼
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。
上面提到了Reflection,會致使APK體積增大,因此Moshi還提供了另一種解析方式,就是註解,Moshi的官方叫法叫作Codegen,由於是採用註解生成的,因此除了添加Moshi的Kotlin依賴以外,還須要加上kapt
kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.8.0'
複製代碼
給咱們的數據類增長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是Moshi有別於Gson,FastJson的最大特色,顧名思義,這是一個Json的轉換器,他的主要做用在於將拿到的Json數據轉換成任意你想要的類型,Moshi內置了不少JsonAdapter,有以下這些:
對於一些比較簡單規範的數據,使用Moshi內置的JsonAdapter已經徹底可以Cover住,可是因爲Json只支持基本數據類型傳輸,因此不少時候不能知足業務上須要,舉個例子:
{
"type": 2,
"isGood": 1
"title": "TW9zaGkgaXMgZmxleGlibGU="
}
複製代碼
這是一個很普通的Json,包含了5個字段,咱們若是按照服務端返回的字段來定義解析的Bean,顯然是能夠徹底解析的,可是咱們在實際調用的時候,這些數據並非很乾淨,咱們還須要處理一下:
對於客戶端的同窗來講,好像沒毛病,之前都是這麼幹的,若是這種不乾淨的Json少點還好,多了以後就很頭疼,每一個在用的時候都須要轉一遍,不少時候我這麼幹的時候都以爲浪費時間,而今天有了Moshi以後,咱們只須要針對須要轉換的類型定義對應的JsonAdapter,達到一次定義,一勞永逸的效果,Moshi針對常見的數據類型已經定義了Adapter,可是內置的Adapter如今已經不能知足咱們的需求了,因此咱們須要自定義JsonAdapter。
class ConfigBean {
public CustomType type;
public Boolean isGood;
public String title;
}
複製代碼
此處咱們定義的數據類型不是根據服務器返回的Json數據,而是定義的咱們業務須要的格式,那麼最終是經過JsonAdapter轉換器來完成這個轉換,下面開始自定義JsonAdapter。
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繼承自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舉個例子,別的基本上都是照葫蘆畫瓢,沒什麼難度
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()));
}
}
複製代碼
public class BooleanAdapter {
@FromJson
public Boolean fromJson(int value) {
return value == 1;
}
@ToJson
public Integer toJson(Boolean value) {
return value ? 1 : 0;
}
}
複製代碼
下面咱們來測試一下
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解析重點分析一下。
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(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,點進去看一下
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<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,既然命中了他,那麼咱們就看一下他的具體實現
構造方法
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方法
對於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更加靈活,而且能夠採用註解自動生成。
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);
}
複製代碼
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();
}
}
複製代碼
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生成也就是註解的方式,跟反射對比一下,會發現有以下優勢:
在進行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中進行更新,你們能夠關注一下。