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 内存布局示意图 头部信息(对象数、fd数等) int (4B) long (8B) String (len+data) Parcelable 对象 文件描述符 更多数据... mDataPos 读写指针(当前指向位置) 写入时指针后移,读取时指针前移 数据连续存储,无分隔符,读写顺序必须严格一致

从图中可以看到,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。