一、什么是序列化
序列化(Serialization)是将对象的状态信息转换为可存储或传输的形式的过程。反序列化(Deserialization)是其逆过程——将字节序列恢复为对象。
在 Java 生态中,序列化主要应用于以下场景:
- 进程间通信(IPC):Android 的 Intent 通过 Binder 传递数据,Binder 使用 Parcelable 将对象序列化后跨进程传输。
- 持久化存储:将对象保存到文件(如 SharedPreferences、SQLite、文件缓存)。
- 网络传输:将对象转换为 JSON/XML/Protobuf 等格式发送到服务端。
- 深拷贝:通过序列化-反序列化实现对象的完整克隆。
二、Java Serializable
2.1 基本使用
import java.io.Serializable; |
2.2 serialVersionUID 详解
serialVersionUID 是序列化版本控制的关键。它的作用是:
- 序列化时,对象的
serialVersionUID被写入序列化流 - 反序列化时,JVM 将流中的
serialVersionUID与当前类的serialVersionUID比较 - 如果两者不一致,抛出
InvalidClassException
如果未显式声明 serialVersionUID?
JVM 会根据类的结构(类名、接口、字段、方法签名等)自动计算一个哈希值作为默认的 serialVersionUID。问题是这个哈希值对类结构的变化极其敏感——哪怕增加一个空格或注释(虽然在字节码层面注释不影响),增加或删除一个字段都会导致新哈希值不同,从而无法反序列化旧版本的序列化数据。
最佳实践:始终显式声明 serialVersionUID,并在类结构调整时手动更新:
private static final long serialVersionUID = 20200101L; // 使用日期作为版本号 |
2.3 transient 关键字
被 transient 修饰的字段不会被默认序列化机制写入流中。典型使用场景:
public class UserSession implements Serializable { |
对于 transient 字段,反序列化后其值为类型的默认值(引用类型为 null,int 为 0)。
2.4 自定义序列化:writeObject/readObject
通过定义私有的 writeObject 和 readObject 方法,可以自定义序列化行为:
public class SecureUser implements Serializable { |
调用时机:
writeObject在ObjectOutputStream.writeObject()内部通过反射调用readObject在ObjectInputStream.readObject()内部通过反射调用
2.5 Externalizable 接口
Externalizable 继承自 Serializable,它要求开发者完全手动控制序列化逻辑:
public class Product implements Externalizable { |
Externalizable vs Serializable:
| 维度 | Serializable | Externalizable |
|---|---|---|
| 实现方式 | JVM 自动处理 | 完全手动 |
| 性能 | 使用反射,较慢 | 无反射,快 |
| 控制粒度 | 通过 transient 排除字段 | 精确控制每个字段 |
| 构造方法 | 不调用构造方法 | 必须有无参 public 构造方法 |
| 序列化数据量 | 包含类的元信息,较大 | 只包含你写的数据,更小 |
2.6 序列化格式分析
Java 序列化产生的二进制格式遵循特定规范。以序列化一个简单的 User 对象为例:
偏移量 Hex 说明 |
格式分析可以从 ObjectInputStream 源码中找到完整的解析逻辑。HotSpot 中相关的 native 方法在 src/java.base/share/native/libjava/ObjectInputStream.c 中。
2.7 序列化安全问题
Java 序列化有严重的安全隐患,是许多远程代码执行(RCE)漏洞的根源:
// 反序列化漏洞示例 |
防护措施:
- 使用
ObjectInputFilter(JDK 9+)限制可反序列化的类 - 避免使用 Java 原生序列化传输不可信数据
- 优先使用 JSON、Protobuf 等安全的序列化格式
- 在 Android 中,跨进程通信使用 Parcelable(类型安全,不会导致任意代码执行)
三、Android Parcelable
3.1 为什么需要 Parcelable
在 Android 中,跨进程传输数据主要通过 Binder 机制。Binder 的事务缓冲区只有 1MB(并且是所有 Binder 调用共享的),因此需要一种内存高效、速度快的序列化方式。Serializable 使用反射 + 大量元数据,序列化后的数据体积大、速度慢,不适合 Binder 传输。
Parcelable 专为 Android IPC 设计:
- 零反射:所有序列化逻辑由开发者显式编写(或通过 APT 生成)
- 紧凑格式:不存储类名、字段名等元信息,只存数据
- 与 Binder 深度集成:Parcel 是 Binder 通信的直接载体
3.2 Parcelable 实现
public class User implements Parcelable { |
注意:writeToParcel 和 CREATOR 的 createFromParcel 中的读写顺序必须严格一致——这是手动序列化最大的坑。
3.3 Parcelable 的性能优势
关于 “Parcelable 比 Serializable 快 10 倍” 的说法:
| 指标 | Serializable | Parcelable |
|---|---|---|
| 序列化方式 | 反射(慢) | 手写代码(快) |
| 数据体积 | 类名、字段名等元信息 | 仅数据值 |
| 对象创建 | 不经过构造方法 | 经过构造方法 |
| 跨平台兼容 | JVM 通用 | Android 专有 |
| 版本兼容 | serialVersionUID 机制 | 需自己处理 |
实际测试数据(简单对象,10000 次序列化):
- Serializable: ~300ms
- Parcelable: ~20ms
- 性能差距约 10-15 倍
3.4 使用 Kotlin 的 @Parcelize
Kotlin 提供了 @Parcelize 注解,通过编译器插件自动生成 Parcelable 实现,无需手写繁琐的 writeToParcel:
import kotlinx.parcelize.Parcelize |
Kotlin 编译器会在 class 文件中自动生成 writeToParcel 和 CREATOR 的实现。这是 Android 上最推荐的 Parcelable 实现方式。
四、JSON 序列化
4.1 Gson 原理
Gson 是 Google 开发的 Java JSON 序列化库。其核心原理是通过反射读取对象的字段,然后递归序列化:
// 序列化 |
Gson 的内部机制:
toJson()→ 获取对象的TypeToken→ 创建TypeAdapter- TypeAdapter 通过反射(
Field.get())读取每个字段的值 - 对于复杂字段(嵌套对象、集合),递归创建子 TypeAdapter
- 最终通过
JsonWriter输出 JSON 字符串
Gson 的类型擦除问题:
由于 Java 泛型擦除,以下代码会有问题:
// 错误!反序列化后得到的是 LinkedTreeMap 而非 User |
4.2 Moshi vs Gson vs kotlinx.serialization
| 维度 | Gson | Moshi | kotlinx.serialization |
|---|---|---|---|
| 反射 | 使用反射 | 使用反射 + CodeGen | 编译期代码生成(无反射) |
| Kotlin 支持 | 一般 (需额外处理 null safety) | 好 (Kotlin 感知) | 原生 Kotlin |
| 性能 | 中 | 中 (CodeGen 快) | 快(无反射) |
| Android 推荐 | 传统项目 | OkHttp 系列项目 | 现代 Kotlin 项目 |
为什么 kotlinx.serialization 最快?
它完全在编译期生成序列化代码,运行时零反射:
|
4.3 FastJson 的安全性问题
FastJson 是阿里巴巴开源的 JSON 库,在早期因其高性能被广泛使用。但它存在严重的安全漏洞(反序列化 RCE):
- 通过
@type字段指定类名,FastJson 会尝试实例化任意类 - 结合特定类的 setter/getter 方法链,可以触发任意代码执行
- 修复方案:升级到最新版本、开启
SafeMode、使用autoType白名单
Android 开发建议:优先使用 Gson(稳定)、Moshi(Kotlin)、kotlinx.serialization(现代 Kotlin)。
五、Protobuf(Protocol Buffers)
Protobuf 是 Google 开发的语言中立、平台中立的可扩展序列化格式。相比 JSON,它有几个显著优势:
5.1 定义消息格式
// user.proto |
5.2 编码格式
Protobuf 使用 Tag-Length-Value (TLV) 编码:
字段编码 = (field_number << 3) | wire_type |
Varint 编码:一种变长整数编码,小数字使用更少的字节:
数字 300 的 Varint 编码: |
5.3 Protobuf vs JSON 对比
| 维度 | JSON | Protobuf |
|---|---|---|
| 可读性 | 人可读 | 二进制,不可读 |
| 体积 | 较大(字段名重复) | 小(字段序号代替字段名) |
| 解析速度 | 慢(文本解析) | 快(二进制解析) |
| Schema | 无(弱类型) | 有(强类型,通过 .proto 定义) |
| 版本兼容 | 手动处理 | 内置向前/向后兼容 |
| Android 支持 | 全平台 | 需要 proto 编译器和运行时 |
| 适用场景 | REST API、配置文件 | RPC(gRPC)、高性能存储 |
六、各序列化方案的适用场景
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| Intent/Bundle 传递数据 | Parcelable | 与 Binder 集成,速度快 |
| 网络 API 通信 | JSON (Moshi/Gson) | 人可读,跨语言 |
| 微服务 RPC 通信 | Protobuf + gRPC | 高性能,强类型 |
| 本地持久化(简单) | JSON / SharedPreferences | 实现简单 |
| 本地持久化(大量) | Protobuf / Room (SQLite) | 读写入性能好 |
| 缓存数据 | Serialization (LruCache 应用类) | 深度拷贝方便 |
| 对象深拷贝 | Serialization | 一行代码完成 |
| 跨进程大数据 | Parcelable + ContentProvider | Binder 限制 1MB |
七、源码路径
| 组件 | 路径 |
|---|---|
| Java Serializable (JDK) | jdk/src/java.base/share/classes/java/io/ObjectOutputStream.java |
| Parcelable | frameworks/base/core/java/android/os/Parcelable.java |
| Parcel (native) | frameworks/native/libs/binder/Parcel.cpp |
| Parcel (Java) | frameworks/base/core/java/android/os/Parcel.java |
| Gson | gson/gson/src/main/java/com/google/gson/ |
| kotlinx.serialization | kotlin/kotlinx.serialization/ |
八、常见面试题
Q1: Parcelable 和 Serializable 有什么区别?为什么 Android 推荐使用 Parcelable?
A: 核心区别:Serializable 是 Java 标准序列化机制,使用反射自动序列化对象的所有字段,序列化后的数据包含类名、字段名等元信息,体积大且速度慢。Parcelable 是 Android 专有的序列化接口,要求开发者显式编写序列化代码,序列化后的数据只包含字段值,不含元信息,因此体积更小、速度更快(通常快 10-15 倍)。Android 推荐 Parcelable 的原因是:(1) Binder 的事务缓冲区只有 1MB,Parcelable 的紧凑格式更适合;(2) Parcelable 不经过反射,性能更好;(3) 跨进程通信是 Android 应用的核心场景(Activity 跳转、Service 绑定等),Parcelable 与 Binder 深度集成。缺点是 Parcelable 需要手写大量样板代码(Kotlin 的 @Parcelize 解决了这个问题)。
Q2: serialVersionUID 的作用是什么?不设置会怎样?
A: serialVersionUID 是序列化版本控制的标识符。序列化时,它会随对象数据一同写入流中。反序列化时,JVM 会比较流中的 serialVersionUID 与本地类的 serialVersionUID,如果不一致则抛出 InvalidClassException。如果不设置,JVM 会根据类的结构(类名、字段、方法签名等)通过 SHA 算法自动生成一个。问题在于自动生成的值对类结构非常敏感——增加一个字段、改变一个方法签名都会导致生成的 serialVersionUID 不同,从而导致旧版本序列化的数据无法反序列化。因此最佳实践是显式声明一个 serialVersionUID 并在类演化时保持兼容性。
Q3: 什么是 transient 关键字?反序列化后 transient 字段的值是什么?
A: transient 是 Java 的关键字,用于标记不需要序列化的字段。被标记的字段在序列化时会被忽略,反序列化后其值为该类型的默认值:引用类型为 null,int/short/byte/long/float/double 为 0,boolean 为 false,char 为 ‘ ’。典型使用场景:(1) 安全敏感数据(密码、Token);(2) 运行时状态(ThreadLocal、当前登录状态);(3) 可重新计算的派生字段(缓存值、聚合结果);(4) 不可序列化的字段(如 Context、View 等 Android 组件)。如果需要给 transient 字段赋予合理的反序列化值,可以在 readObject() 方法中手动处理。
Q4: Binder 的事务缓冲区为什么只有 1MB?传输大数据时怎么办?
A: Binder 的 1MB 限制是 Android 系统设计的一个安全阈值(定义在 frameworks/native/libs/binder/ProcessState.cpp 中的 BINDER_VM_SIZE),目的是防止单个 Binder 调用占用过多内核内存影响系统稳定性。当需要传输大数据时:(1) 对于图片/文件,使用 ContentProvider + 文件描述符(通过 Parcel 发送 ParcelFileDescriptor);(2) 对于大量结构化数据,使用 ContentProvider + Cursor;(3) 使用 MemoryFile(匿名共享内存,ashmem)共享大块数据,通过 Binder 只传递文件描述符;(4) 使用 Messenger + Bundle(Binder 限制的微妙差异,Bundle 的 putBinder 没有 1MB 限制但也不推荐大数据)。注意:1MB 是所有并发 Binder 调用共享的,实际可用空间可能小于 1MB,如果同时有多个 Binder 调用在进行,你的可用缓冲区会更小。
Q5: Gson 如何处理泛型?为什么 TypeToken 能保留泛型信息而直接传 Class 不能?
A: Java 的泛型在编译期会被擦除(Type Erasure),List<User>.class 在运行时不存在,它只是 List.class。因此在 gson.fromJson(json, List.class) 中,Gson 只能知道这是一个 List,但不知道 List 的元素类型,只能默认为 LinkedTreeMap(Map 类型)。TypeToken 使用了匿名内部类的技巧:new TypeToken<List<User>>(){} 创建了一个匿名子类,JVM 保留了其父类的泛型签名在 class 文件的 Signature 属性中(虽然字节码层面泛型被擦除,但 Signature 属性保留了完整的泛型信息)。Gson 通过 getGenericSuperclass() 读取这个 Signature 属性,获取到完整的 List<User> 类型信息。这就是 TypeToken 能够保留泛型信息的原理。
Q6: 反序列化漏洞(RCE)的原理是什么?Android 中是否存在类似的风险?
A: Java 反序列化 RCE 的典型攻击链:攻击者构造一个特殊的序列化字节流,其中包含一个精心设计的对象图。当 readObject() 被调用时,JVM 按照字节流中的指令逐步创建对象并调用它们的 readObject、readResolve 等方法。如果某个类的这些方法中存在危险的逻辑(如执行系统命令、加载远程类、调用 Runtime.exec()),就会形成攻击链。著名的利用链包括 Commons Collections、Spring Beans、JDK 内置的 AnnotationInvocationHandler 等。在 Android 中,原生序列化(Serializable)的风险较小,因为:(1) Android 不包含 Java EE 的那些危险类(如 Commons Collections);(2) Binder 的 Parcelable 是类型安全的——deserialize 时明确指定了类(通过 CREATOR),不会像 Java 原生序列化那样从流中动态加载任意类;(3) Android 9+ 对 ObjectInputStream 增加了额外的安全检查。但不意味着绝对安全——如果你的应用使用了存在漏洞的第三方库,仍可能受到影响。
参考文档:
- Java Object Serialization Specification: https://docs.oracle.com/javase/8/docs/platform/serialization/spec/serialTOC.html
- Android Parcelable: https://developer.android.com/reference/android/os/Parcelable
- Protocol Buffers: https://developers.google.com/protocol-buffers

