新一代Json解析库Moshi使用及原理解析

概述

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的地址是https://api.douban.com/v2/movie/top250?start=0&count=100,这个是返回豆瓣电影评分排名前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的构造函数,导致默认值会失效,下面举个例子:

@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

基本用法之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转换
  • 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<ConfigBean>,我们看看是怎么生成的,跟一下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中进行更新,大家可以关注一下。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容