4、Binder数据传输:Parcel机制、基本类型传输、自定义对象传输、文件描述符传递

Binder 通信的核心,说白了就是数据怎么打包、怎么传、怎么拆。

我刚开始接触 Binder 时,总觉得它很神秘。后来发现,它本质上就是个快递系统——你把东西装进包裹(Parcel),快递员(Binder 驱动)送到对方手里,对方拆包取货。今天我们就来拆开这个包裹,看看里面到底怎么玩。

4.1 Parcel 机制:Binder 的数据快递箱

Parcel 是什么?它是 Android 系统里专门为 Binder 设计的数据容器。你可以把它理解成一个序列化缓冲区,支持跨进程传输。

我个人习惯把 Parcel 比作一个「万能口袋」。你往里塞基本类型、塞对象、塞文件描述符,它都能装。到了对端进程,再按顺序掏出来。

核心要点:Parcel 是进程间数据传输的载体,数据写入和读取的顺序必须一致。

为什么必须一致?因为 Parcel 内部就是个字节流。你写了个 int,再写了个 String,对方就得先读 int,再读 String。顺序乱了,数据就全错了。我在项目中遇到过好几次这种问题,排查起来特别头疼。

4.2 基本类型传输:int、long、String 怎么传

基本类型的传输是最简单的。Parcel 提供了现成的 API:

// 写入端
Parcel data = Parcel.obtain();
data.writeInt(42);
data.writeLong(100L);
data.writeString("Hello Binder");
data.writeFloat(3.14f);

// 读取端
int intVal = data.readInt();
long longVal = data.readLong();
String strVal = data.readString();
float floatVal = data.readFloat();

你看,就这么几行代码。但有个坑要注意:写入和读取的类型必须严格对应。你写了个 int,就不能用 readLong 去读,否则数据会错位。

避坑指南:我曾经在项目中犯过一个低级错误——写入时用了 writeInt,读取时用了 readFloat。结果数据完全不对,调试了一下午才发现是类型不匹配。记住,Parcel 不检查类型,它只按字节流处理。

另外,Parcel 内部有自动扩容机制。你写入的数据量超过初始容量时,它会自动申请更大的内存。但频繁扩容会影响性能,所以我建议:如果知道数据量较大,可以提前调用 data.setDataCapacity() 预分配空间。

4.3 自定义对象传输:Parcelable 与 Serializable

实际项目中,我们传的往往不是基本类型,而是自定义对象。比如一个 User 对象,包含姓名、年龄、地址等字段。

Android 提供了两种序列化方式:Parcelable 和 Serializable。但 Binder 通信中,官方推荐使用 Parcelable

为什么?因为 Serializable 基于 Java 反射,效率低,而且会产生大量临时对象,触发 GC。Parcelable 是手动编码,性能高得多。

来看一个 Parcelable 的实现示例:

public class User implements Parcelable {
    private String name;
    private int age;
    private String address;

    public User(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    protected User(Parcel in) {
        name = in.readString();
        age = in.readInt();
        address = in.readString();
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeInt(age);
        dest.writeString(address);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    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 和构造方法中的读取顺序必须一致。我见过有人写反了,结果数据全乱套。
  • describeContents() 一般返回 0。只有包含文件描述符时才返回特殊值。
  • CREATOR 是必须的。Binder 驱动通过它来反序列化对象。

个人经验:如果你用 Android Studio,可以安装 Parcelable 插件。一键生成模板代码,省时省力。但生成后记得检查一下字段顺序,插件有时候会搞错。

那什么时候用 Serializable?我个人建议:只在本地进程内使用,或者数据量很小、不频繁传输的场景。跨进程 Binder 通信,老老实实用 Parcelable。

4.4 文件描述符传递:跨进程共享文件

文件描述符(File Descriptor,简称 FD)是 Linux 系统里操作文件的句柄。Binder 支持跨进程传递 FD,这意味着你可以把一个打开的文件「借」给另一个进程使用。

这个功能很强大。比如一个进程打开了某个设备文件,另一个进程想读写它,不需要重新打开,直接通过 Binder 传递 FD 就行。

看代码:

// 服务端:打开文件并传递 FD
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();

try {
    FileDescriptor fd = ParcelFileDescriptor.open(
        new File("/data/local/tmp/test.txt"),
        ParcelFileDescriptor.MODE_READ_WRITE
    ).getFileDescriptor();

    data.writeFileDescriptor(fd);
    mRemote.transact(TRANSACTION_GET_FD, data, reply, 0);
} catch (Exception e) {
    e.printStackTrace();
} finally {
    data.recycle();
    reply.recycle();
}

// 客户端:接收 FD 并使用
ParcelFileDescriptor pfd = reply.readFileDescriptor();
FileInputStream fis = new FileInputStream(pfd.getFileDescriptor());
// 读取文件内容...

重要提醒:传递 FD 时,Binder 驱动会自动增加引用计数。接收方使用完毕后,必须调用 close() 释放。否则会导致文件描述符泄漏。我曾经在线上环境遇到过这个问题——进程的 FD 数量不断增长,最终触发系统限制,导致服务崩溃。

另外,传递 FD 时要注意权限问题。接收方进程需要有对应文件的读写权限,否则即使拿到了 FD,操作也会失败。

4.5 性能优化:Parcel 复用与池化

频繁创建和回收 Parcel 对象会产生大量内存碎片。我建议使用 Parcel 池来复用对象。

Android 框架层其实已经做了这件事——Parcel.obtain() 就是从全局池中获取 Parcel 实例。但如果你在频繁调用的场景中(比如每秒几百次 Binder 调用),可以考虑自己维护一个池。

// 简单的 Parcel 池示例
public class ParcelPool {
    private static final int MAX_POOL_SIZE = 10;
    private static final LinkedList<Parcel> sPool = new LinkedList<>();

    public static Parcel obtain() {
        synchronized (sPool) {
            if (!sPool.isEmpty()) {
                return sPool.removeFirst();
            }
        }
        return Parcel.obtain();
    }

    public static void recycle(Parcel parcel) {
        parcel.setDataPosition(0);
        parcel.setDataSize(0);
        synchronized (sPool) {
            if (sPool.size() < MAX_POOL_SIZE) {
                sPool.add(parcel);
            } else {
                parcel.recycle();
            }
        }
    }
}

这个池子虽然简单,但在高并发场景下能明显减少 GC 压力。我在做系统级服务优化时用过类似方案,效果不错。

4.6 总结

好了,这一章的内容就这些。我们来回顾一下:

  • Parcel 机制:Binder 的数据容器,读写顺序必须一致。
  • 基本类型传输:直接用 writeInt/readInt 等 API,注意类型匹配。
  • 自定义对象传输:用 Parcelable,别用 Serializable。
  • 文件描述符传递:跨进程共享文件,记得关闭释放。
  • 性能优化:复用 Parcel,减少内存分配。

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