42. Parcel 序列化:Parcel 类如何实现数据的序列化与反序列化?
说到 Binder 通信,Parcel 是绕不开的核心角色。说白了,Parcel 就是 Binder 的「快递箱」—— 你要跨进程传数据,就得先把数据打包好,扔进这个箱子,对方收到后再拆包取出。
我刚开始接触 Android 源码时,总觉得 Parcel 就是个简单的序列化工具。后来踩过几次坑才明白,它的设计远比想象中精巧。今天咱们就把它拆开看看。
Parcel 的本质是什么?
Parcel 本质上是一个 内存缓冲区,支持扁平化数据的读写。它不像 Java 的 Serializable 那样依赖反射,也不像 JSON 那样有文本开销。Parcel 走的是「二进制 + 紧凑布局」路线,性能极高。
我个人习惯把 Parcel 理解为「一块连续的内存 + 一个读写指针」。写数据时指针后移,读数据时指针前移。就这么简单。
核心特点:
- 支持基本类型(int、long、float 等)
- 支持复杂对象(需实现 Parcelable 接口)
- 支持文件描述符(Binder 传 fd 的利器)
- 支持 Native 对象(通过 JNI 桥接)
序列化:数据如何写入 Parcel?
写入操作的核心是 writeXxx() 系列方法。咱们看个典型场景:
// 创建一个 Parcel 对象
Parcel data = Parcel.obtain();
// 写入基本类型
data.writeInt(42);
data.writeLong(100L);
data.writeFloat(3.14f);
// 写入字符串
data.writeString("Hello Binder");
// 写入 Parcelable 对象
User user = new User("张三", 28);
data.writeParcelable(user, 0);
// 写入文件描述符
ParcelFileDescriptor pfd = ...;
data.writeFileDescriptor(pfd.getFileDescriptor());
你可能会问:这些数据在内存里到底怎么存的?我举个例子:
写入一个 int 值 42,Parcel 会在缓冲区末尾追加 4 个字节,内容就是 0x0000002A。写入字符串 "Hello Binder",它会先写字符串长度(int),再写 UTF-8 编码的字节数据。说白了就是「长度 + 内容」的套路。
我的经验: 写数据时尽量保持类型一致。我曾经在项目中遇到过读端用 readInt() 去读一个 writeLong() 写的数据,结果数据错位,排查了半天。Parcel 不会帮你做类型校验,全靠开发者自觉。
反序列化:数据如何从 Parcel 读出?
读操作和写操作完全对称。你写的时候用了 writeInt,读的时候就得用 readInt。顺序也必须一致。
// 从 Parcel 中读取数据
int intVal = data.readInt();
long longVal = data.readLong();
float floatVal = data.readFloat();
String strVal = data.readString();
// 读取 Parcelable 对象
ClassLoader classLoader = getClass().getClassLoader();
User user = data.readParcelable(classLoader);
// 读取文件描述符
ParcelFileDescriptor pfd = data.readFileDescriptor();
这里有个关键点:读写顺序必须严格对应。如果你先写了 int 再写 String,那读的时候也必须先读 int 再读 String。否则数据就全乱了。
注意: Parcel 内部维护了一个 mDataPos 指针。每读一个字段,指针就向后移动相应字节。如果你读多了或读少了,后续数据全部错位。我曾经在调试一个 ANR 问题时发现,就是因为某个字段的读写长度不一致,导致 Binder 线程卡死。
Parcel 的底层存储结构
咱们用一张图来看看 Parcel 的内存布局:
从图中可以看到,Parcel 的数据是连续存储的。头部记录了对象数量、文件描述符数量等元信息。数据区就是纯粹的二进制流。
Parcelable 接口:自定义对象的序列化
如果你想让自定义对象通过 Parcel 传输,就得实现 Parcelable 接口。这是 Android 特有的序列化方式,比 Java 的 Serializable 高效得多。
public class User implements Parcelable {
private String name;
private int age;
// 构造函数
public User(String name, int age) {
this.name = name;
this.age = age;
}
// 从 Parcel 中读取数据(反序列化)
protected User(Parcel in) {
name = in.readString();
age = in.readInt();
}
// 写入数据到 Parcel(序列化)
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(age);
}
// 内容描述(一般返回 0)
@Override
public int describeContents() {
return 0;
}
// 必须的 CREATOR 字段
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
}
这里有个细节:writeToParcel() 和 createFromParcel() 中的读写顺序必须完全一致。我见过不少新手在这里栽跟头 —— 写的时候先写 name 再写 age,读的时候先读 age 再读 name,结果数据全乱了。
我的建议: 写 Parcelable 时,最好把读写逻辑放在相邻的位置,或者用注释标明顺序。比如:
// 写入顺序:name -> age
dest.writeString(name);
dest.writeInt(age);
// 读取顺序:name -> age
name = in.readString();
age = in.readInt();
Parcel 的复用与回收
Parcel 对象不是 new 出来的,而是通过 Parcel.obtain() 从对象池中获取。用完要调用 recycle() 归还。
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
// 写入数据
data.writeInterfaceToken(serviceDescriptor);
data.writeString("hello");
// 执行 Binder 调用
mRemote.transact(TRANSACTION_CODE, data, reply, 0);
// 读取结果
reply.readException();
String result = reply.readString();
} finally {
// 回收 Parcel
data.recycle();
reply.recycle();
}
为什么要用对象池?因为 Binder 通信非常频繁,每次 new 一个 Parcel 对象开销太大。对象池机制能显著减少内存分配和 GC 压力。
我曾经踩过的坑: 有一次在循环中频繁创建 Parcel 对象,忘了 recycle(),结果内存暴涨,直接 OOM。后来排查发现,Parcel 对象池最大容量是 6 个,超过后新创建的 Parcel 不会被回收,导致内存泄漏。所以一定要记得 recycle()。
Parcel 的性能优势
咱们用表格对比一下 Parcel 和 Java Serializable:
| 特性 | Parcel | Serializable |
|---|---|---|
| 序列化方式 | 手动编码(高效) | 反射(低效) |
| 数据格式 | 二进制紧凑 | 带类型信息的二进制 |
| 性能 | 快(约快 10 倍) | 慢 |
| 内存占用 | 小 | 大(含反射元数据) |
| 跨进程支持 | 原生支持 | 不支持 |
说白了,Parcel 就是为 Android 跨进程通信量身定做的。它牺牲了通用性,换来了极致的性能。
总结一下
Parcel 的序列化机制,核心就是「连续内存 + 读写指针」。写数据时按顺序追加,读数据时按顺序取出。没有分隔符,没有类型校验,全靠开发者保证读写顺序一致。
嗯,这里要注意:Parcel 不是线程安全的。如果你在多线程环境下共享同一个 Parcel 对象,记得加锁。不过在实际开发中,Parcel 通常只在单个 Binder 调用生命周期内使用,很少出现多线程竞争的情况。
最后说一句:Parcel 的设计思想其实很简单,但正是这种简单,让它成为了 Android 跨进程通信的基石。理解它,你就能更好地驾驭 Binder。