1. 为什么需要序列化?
Binder 通信的本质,说白了就是两个进程之间搬运数据。但有个问题——进程有自己独立的内存空间,你不能直接把一个 Java 对象的内存地址扔给另一个进程。那怎么办?
你得把对象「拆开」,变成一堆字节流,传到对面再「拼回去」。这个过程,就是序列化与反序列化。
我刚开始接触 Binder 时,觉得这步挺简单的。后来踩了几个坑才发现,序列化写不好,轻则性能崩盘,重则直接死给你看。
2. Parcel:Binder 的快递箱
Android 里负责序列化的核心类叫 Parcel。你可以把它想象成一个快递箱——你把数据往里塞,封好,Binder 驱动帮你运到对面,对面再拆箱取出来。
Parcel 支持的数据类型很丰富:
- 基本类型:int、long、float、double、boolean
- 字符串:String、String[]
- 文件描述符:ParcelFileDescriptor
- Binder 对象:IBinder
- Parcelable 对象:你自己定义的数据结构
来看一个最简单的读写例子:
// 写端
Parcel data = Parcel.obtain();
data.writeInt(42);
data.writeString("Hello Binder");
data.writeFloat(3.14f);
// 读端
Parcel reply = Parcel.obtain();
// ... 经过 Binder 传输后 ...
int val = reply.readInt();
String str = reply.readString();
float f = reply.readFloat();
嗯,这里要注意:读写顺序必须完全一致。你先写 int 再写 string,对面就必须先读 int 再读 string。顺序乱了,数据就全歪了。我在项目中见过好几次这种 bug,排查起来特别头疼。
3. Parcel 的底层结构
Parcel 内部维护了一块连续的内存缓冲区。它用两个指针来管理:
- 数据指针:指向当前要读/写的位置
- 容量指针:指向缓冲区末尾
写数据时,数据指针往后移动;读数据时也一样。如果数据量超过当前容量,Parcel 会自动扩容——跟 ArrayList 的机制有点像。
我个人习惯把 Parcel 的布局想象成下面这样:
| 数据区 | 对象引用区 | 空闲区 |
|--------|------------|--------|
| 写指针 → | 容量指针 → |
数据区存的是基本类型和原始字节。对象引用区存的是 Binder 代理、文件描述符这些「跨进程引用」。为什么分开?因为 Binder 驱动在传输时,对这两类数据的处理方式完全不同。
核心要点:Parcel 不是简单的字节数组。它内部维护了「数据」和「引用」两张表,Binder 驱动在传输时会分别处理。
4. 自定义 Parcelable:让对象也能跨进程
光传 int 和 string 肯定不够用。你肯定希望把自己的数据类也传过去。这就需要实现 Parcelable 接口。
我建议你记住一个口诀:「写三样,读一样」。
- 写三样:实现
writeToParcel()、定义CREATOR、定义describeContents() - 读一样:从 Parcel 中构造对象
来看一个完整的例子:
public class User implements Parcelable {
private String name;
private int age;
private boolean isVip;
// 1. 普通构造方法
public User(String name, int age, boolean isVip) {
this.name = name;
this.age = age;
this.isVip = isVip;
}
// 2. 从 Parcel 读取(反序列化)
protected User(Parcel in) {
name = in.readString();
age = in.readInt();
isVip = in.readByte() != 0;
}
// 3. 写入 Parcel(序列化)
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(age);
dest.writeByte((byte) (isVip ? 1 : 0));
}
// 4. describeContents:一般返回 0
@Override
public int describeContents() {
return 0;
}
// 5. 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];
}
};
}
小技巧:Android Studio 有 Parcelable 插件,一键生成代码。但我建议你手写几次,理解原理后再用插件。不然出了问题你都不知道怎么修。
5. 避坑指南:我踩过的那些坑
我曾经在一个项目里,把一个包含 List<String> 的对象序列化后传过去,结果对面读出来全是 null。排查了半天才发现问题——
坑一:读写顺序不一致
写的时候先写 name 再写 age,读的时候先读 age 再读 name。数据全错位了。这种 bug 编译器检查不出来,只能靠人眼对。
坑二:boolean 的读写
Parcel 没有直接的 readBoolean() 和 writeBoolean()。你得用 writeByte() 和 readByte() 来模拟。我见过有人直接 writeInt(1) 和 readInt(),虽然也能用,但浪费了 3 个字节。
坑三:Parcel 复用
每次通信都 obtain() 一个新 Parcel,用完后记得 recycle()。不然内存泄漏等着你。我早期代码里就漏过 recycle,结果跑了一天后 OOM 了。
警告:Parcel 不是线程安全的。多线程同时读写同一个 Parcel,数据会乱。每个线程请使用独立的 Parcel 实例。
6. Parcelable vs Serializable
你可能会问:Java 不是有 Serializable 吗?为什么 Android 还要搞个 Parcelable?
原因很简单:性能。
| 对比项 | Parcelable | Serializable |
|---|---|---|
| 实现方式 | 手动编码,明确每个字段 | 反射机制,自动处理 |
| 性能 | 快(无反射,无临时对象) | 慢(反射 + 大量临时对象) |
| 代码量 | 稍多,但可控 | 极少,一行注解 |
| 适用场景 | Binder 通信、Intent 传参 | 本地持久化、网络传输 |
我个人的经验是:跨进程通信必须用 Parcelable。Serializable 在 Binder 里也能用,但性能差一个数量级。你想想看,每次 Binder 调用都要反射一遍所有字段,那画面太美我不敢看。
7. 核心流程图:Parcel 的读写生命周期
下面这张图展示了 Parcel 从创建到回收的完整流程:
这张图把 Parcel 的完整生命周期串起来了。你注意看第三步——Binder 驱动传输。这一步是内核在做,用户态完全不可控。我们能做的,就是保证前两步和后两步的正确性。
8. 总结
Parcel 的序列化与反序列化,是 Binder 通信的基石。理解它,你才算真正入门了 Binder。
记住几个关键点:
- 读写顺序必须一致
- boolean 用 byte 模拟
- 用完后记得 recycle
- 跨进程通信用 Parcelable,别用 Serializable
下一章我们会深入 Binder 驱动层,看看数据到底是怎么在内核里流转的。到时候你会发现,用户态的 Parcel 只是冰山一角。
公众号:蓝海资料掘金营,微信deep3321