Parcel 与 Transaction:一次事务里装了什么
Parcel 与 Transaction:一次事务里装了什么
上一章我们看到驱动把事务从客户端送到服务端。这一章回答:这个事务里到底装了什么, 参数是怎么写进去、又怎么被读出来的?
驱动能够把事务送到目标进程,但服务端仍需准确还原接口令牌、参数、Binder 引用和文件描述符。 这套协议由 Parcel 的写入顺序与事务元数据共同定义;任一端读写顺序不一致,调用就会错位。
Parcel 不是普通 byte[]
| 常见误解 | 正确理解 |
|---|---|
| Parcel 就是“把对象变成 byte[]” | Parcel 是结构化的写入流,按顺序写类型化数据,还支持对象引用和文件描述符 |
| Binder 传递对象时复制了一份对象 | 传的是 Binder 引用(句柄),不是对象副本;真正数据仍在持有者进程 |
| 读 Parcel 顺序随意 | 服务端读取顺序必须与写入顺序完全一致,否则得到错位或异常 |
Parcel 是 Binder 事务使用的结构化数据容器。它不仅承载标量与字符串,还能在额外的对象 偏移表中记录 Binder 对象和文件描述符。服务端必须按照客户端的写入顺序和类型读取。
核心结论
- 一次事务由三部分组成:目标与调用号(target、code)、标志(flags)、数据(Parcel); 同步调用还会带一个 reply Parcel 装返回值。
- 写入与读取必须同序:
writeInt()之后必须readInt(),writeStrongBinder()之后 必须readStrongBinder()。 - 传递 Binder 对象时,Parcel 记录的是句柄或本地引用,由驱动完成映射,不复制业务对象。
- 文件描述符可以跨进程传递:Parcel 写 fd,内核把它映射到接收进程的 fd 表,大文件因此 不需要把数据全部拷进事务。
- 单次事务有约 1MB 的缓冲上限,超过后抛出
TransactionTooLargeException。
一次事务的装载流程
逐步解释:
- Proxy 按声明顺序写参数:每个参数对应一个写方法,顺序由接口定义固定。
- Parcel 装载完成:
transact()把 data 和记录 Binder 对象位置的 offsets 一起交给驱动。 - 驱动生成
binder_transaction_data:包含目标句柄、调用号 code、标志 flags 和数据区。 - 服务端
onTransact()按 code 分发:同一接口的不同方法对应不同 code。 - 服务端按相同顺序读取:读错顺序是运行时错误,不会自动纠正。
- reply 原路返回:返回值同样经过 Parcel。
对象引用与文件描述符为什么特殊
普通参数(int、String、Parcelable 字段)是真正的数据拷贝。但 writeStrongBinder() 写入的 是 Binder 对象在 Parcel 中的引用位置:驱动看到后,把“句柄”换成接收进程可用的引用, 对象本身仍在原进程。writeFileDescriptor() 同理,内核把 fd 复制到接收进程的 fd 表, 接收方拿到的是指向同一文件的新 fd。
这正是“大文件传 fd、小数据走 Parcel”分工的依据:Parcel 只传“入口”,不传“全部内容”。
源码证据
阅读目标:确认 Parcel 的成对读写,以及事务结构里 data 与 offsets 的分工。
正文指针:Parcel.java writeInt()/readInt()、writeStrongBinder()/readStrongBinder(); 内核 UAPI 头文件 binder.h 中的 binder_transaction_data。
// Parcel.java(伪代码,行号以 r75 为准)
public final void writeInt(int val) { ... }
public final int readInt() { ... }
public final void writeStrongBinder(IBinder val) { ... }
public final IBinder readStrongBinder() { ... }// include/uapi/linux/android/binder.h:事务数据结构(伪代码,行号以 r75 为准)
struct binder_transaction_data {
union { binder_uintptr_t handle; void* ptr; } target;
binder_uint32_t code; // 方法编号
binder_uint32_t flags; // 同步/oneway 等
union { size_t buffer_size; ... } data;
size_t offsets_size; // Binder 对象在 data 中的位置表
};这段代码证明: Parcel 读写必须成对出现;事务结构把“业务数据”和“对象位置表”分开, 驱动只处理对象位置表,不解析业务字段。业务字段怎么装、怎么读,由 AIDL 生成的代码决定。
验证与排障
环境:任意开发设备即可;以下命令不需要 root。
adb logcat -v threadtime | grep -E "TransactionTooLarge|Parcel"
adb shell dumpsys activity service <服务名>TransactionTooLargeException 的典型来源:把大图片、大列表或完整文件内容塞进 Binder 事务。日志特征通常包含抛出异常的调用方堆栈。修复方向:改用文件路径或传文件描述符, 分页查询,或把大数据放到共享内存后只传 fd。
读取顺序错误时,服务端通常会抛 RuntimeException(如“Parcel: unable to marshal”一类), 先在本地写单测复现读写顺序,再怀疑跨进程问题。
常见误区
- “Parcel 像 JSON 一样可以随意取字段”:必须同序读取,没有按名字取值的机制。
- “Binder 对象被复制过去了”:传的是引用/句柄,对象实例仍属于原进程。
- “fd 传过去就是同一个整数”:两个进程的 fd 编号可以不同,但指向同一文件。
- “小数据随便传”:每个事务约 1MB 上限,且拷贝有成本,高频大参数仍然会拖慢调用。
延伸问题
- Parcel 的
dataPosition()、setDataPosition()在什么场景会被用到? Parcelable与Serializable在跨进程传递时有什么本质区别?- offsets 区域为什么必须由驱动校验,普通数据为什么不需要?
- 什么是“Binder 对象泄漏”,它和 Parcel 传对象引用有什么关系?
源码入口
| 文件 | 关键位置 | 作用 |
|---|---|---|
Parcel.java | write* / read* | Java 侧按序读写 |
Parcel.cpp | writeInt32() / readInt32() | Native 侧编解码 |
Parcel.cpp | writeStrongBinder() / readStrongBinder() | Binder 引用编解码 |
binder.h | struct binder_transaction_data | 内核侧事务结构 |
公共路径:Java 侧 frameworks/base/core/java/android/os/,Native 侧 frameworks/native/libs/binder/,内核 UAPI 在 Android 14 公共内核的 include/uapi/linux/android/。行号以 r75 检索为准。
客户端如何按名称获得服务引用,见 ServiceManager。