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 读写生命周期 1. Parcel.obtain() 2. writeInt / writeString / ... 3. Binder 驱动传输(跨进程) 4. readInt / readString / ... 5. Parcel.recycle() 客户端 内核 服务端

这张图把 Parcel 的完整生命周期串起来了。你注意看第三步——Binder 驱动传输。这一步是内核在做,用户态完全不可控。我们能做的,就是保证前两步和后两步的正确性。

8. 总结

Parcel 的序列化与反序列化,是 Binder 通信的基石。理解它,你才算真正入门了 Binder。

记住几个关键点:

  • 读写顺序必须一致
  • boolean 用 byte 模拟
  • 用完后记得 recycle
  • 跨进程通信用 Parcelable,别用 Serializable

下一章我们会深入 Binder 驱动层,看看数据到底是怎么在内核里流转的。到时候你会发现,用户态的 Parcel 只是冰山一角。


公众号:蓝海资料掘金营,微信deep3321