背景与痛点
在 Android 日常开发里,"拉接口→解析 JSON→展示列表" 几乎是固定套路。
可一旦接口字段多、嵌套深,或者后端改个类型,老项目里常见的Gson代码就会暴露三大痛点:
- 类型不安全——
@SerializedName写错一个字母,运行时才崩。 - 反射开销大——启动即扫描所有类,低端机掉帧明显。
ProGuard一压缩,字段对不上,灰度包疯狂崩溃。
于是团队开始物色更轻、对 Kotlin 更友好的替代品,最后锁定了com.squareup.moshi:moshi:1.14.0。
技术选型对比
| 维度 | Gson 2.10 | Jackson 2.15 | Moshi 1.14 |
|---|---|---|---|
| Kotlin 支持 | 靠第三方 | 靠模块 | 官方一等公民 |
| 代码生成 | 无 | 有 | kapt/ksp生成,零反射 |
| 泛型安全 | 运行时擦除 | 部分安全 | 编译期擦除检查 |
| 包体积 | ~280 KB | ~1.5 MB | ~200 KB |
| 线程安全 | 是 | 是 | 是,且JsonAdapter可复用 |
一句话总结:
Gson 老而弥坚,但反射重;Jackson 功能全,但体积大;Moshi 在"体积、安全、速度"三角里找到了甜点,尤其Kotlin codegen让数据类直接免反射,真香。
核心实现:5 分钟跑通
1. 依赖配置
在build.gradle.kts里加两行:
plugins { id("com.google.devtools.ksp") version "1.8.10-1.0.9" // 或 kapt } dependencies { implementation("com.squareup.moshi:moshi:1.14.0") ksp("com.squareup.moshi:moshi-kotlin-codegen:1.14.0") }2. 数据类定义
@JsonClass(generateAdapter = true) // 生成适配器,编译期完成 data class User( @Json(name = "user_id") // 后端字段 snake_case val userId: String, val name: String, val vip: Boolean = false // 缺省值,接口没返也能跑 )3. 解析 / 序列化
val moshi = Moshi.Builder().build() val userAdapter: JsonAdapter<User> = moshi.adapter() // JSON → Object val user = userAdapter.fromJson(jsonString) ?: error("empty payload") // Object → JSON val json = userAdapter.toJson(user)注意:
adapter()是 Kotlin 扩展,泛型实化,省掉手动传TypeToken的麻烦。
高级特性:自定义与多态
1. 自定义 Adapter——处理时间戳
后端返秒级时间戳,但客户端要java.time.LocalDate:
object SecondsToLocalDateAdapter { @FromJson fun fromJson(seconds: Long): LocalDate = Instant.ofEpochSecond(seconds才).atZone(ZoneOffset.systemDefault()).toLocalDate() @ToJson fun toLocalDate(date: LocalDate): Long = date.atStartOfDay(ZoneOffset.systemDefault()).toEpochSecond() } val moshi = Moshi.Builder() .add(SecondsToLocalDateAdapter) // 注册即可 .build()2. 多态类型——支付渠道抽象
@JsonClass(generateAdapter = true) sealed class PayChannel { @JsonClass(generateAdapter = true) data class Wechat(val appId: String) : PayChannel() @JsonClass(generateAdapter = true) data class Alipay(val userId: String) : PayChannel() } val polymorphicAdapter = PolymorphicJsonAdapterFactory .of(PayChannel::class.java, "type") .withSubtype(PayChannel.Wechat::class.java, "WECHAT") .withSubtype(PayChannel.Alipay::class.java, "ALIPAY") val moshi = Moshi.Builder() .add(polymorphicAdapter) .build()这样后端只改"type":"ALIPAY",客户端就能自动反序列化到对应子类,无需手写when判断。
性能与安全
- 内存:
1.14.0 默认启用JsonReader的BufferRecycler池化,解析 1 MB 大数组 GC 次数比 Gson 少 30%。 - 速度:
在 Pixel 4 上循环解析 5 000 次 2 KB 的列表,Moshi codegen 版本平均 1.2 ms,Gson 2.0 ms。 - 线程安全:
Moshi实例本身无状态,可全局单例;JsonAdapter也线程安全,但toJson/fromJson的JsonWriter/JsonReader不要跨线程复用。
避坑指南(生产环境血泪史)
ProGuard / R8
开启代码生成后,仍需保留生成的*JsonAdapter类:-keep class **JsonAdapter { *; } -keep @com.squareup.moshi.JsonClass class * { *; }泛型擦除
运行时想解析List<User>,不能直接adapter<List<User>>(),要用Types.newParameterizedType:val listUserType = Types.newParameterizedType(List::class.java, User::class.java) val adapter = moshi.adapter<List<User>>(listUserType)默认值失效
若后端返回null,而客户端用val count: Int = 0,会抛JsonDataException。解决:- 后端保证不返 null;
- 或者把字段声明为
Int?,再用.or(0)兜住。
混淆后字段对不上
开启R8 fullMode后,内部类名被压缩,多态适配器会找不到type字段。务必加@JsonClass并写全withSubtype。
小结
Moshi 1.14.0 把"编译期生成 + Kotlin 空安全 + 体积轻盈"做成了组合拳,既解决了 Gson 的类型黑洞,又比 Jackson 更轻。
只要记住三件事:
- 所有数据类打
@JsonClass(generateAdapter = true); - 泛型嵌套时用
Types工具类; - 混淆规则别漏,灰度包就能稳稳跑。
思考题
当列表接口一次返 10 000 条数据、每条 20 字段时,如何在不改接口的前提下,利用 Moshi 把内存峰值再降 30%?
(提示:流式解析、自定义 ListAdapter、复用对象池)
欢迎动手实验,把结果贴在评论区一起交流!