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 的驱动层,看看数据到底是怎么在内核空间流转的。到时候你会发现,用户态的这些操作,其实只是冰山一角。