19. 数据包大小:Binder 传输的数据包大小限制与配置方法
Binder 传输数据,到底能传多大?
这个问题,我当年刚接触 Android 系统时也踩过坑。有一次,我在做一个跨进程传输大图片的功能,结果 Binder 调用直接抛了个 TransactionTooLargeException。当时我就懵了——明明内存够用啊,怎么就不让传了?
后来翻源码才明白,Binder 对数据包大小是有硬性限制的。而且这个限制,不是你想改就能随便改的。
19.1 默认限制:1MB 的由来
Binder 驱动在 Linux 内核中,默认给每个 Binder 事务分配了一个最大缓冲区,大小是 1MB(确切地说是 1024 * 1024 字节)。
这个 1MB 是怎么来的?
其实它定义在 /drivers/android/binder.c 中:
#define BINDER_VM_SIZE ((1 * 1024 * 1024) - (4096 * 2))
嗯,实际可用大小是 1MB - 8KB,也就是 1016KB。那 8KB 是留给 Binder 协议头、对象引用等元数据用的。
所以,你真正能传的数据,最多也就 1016KB 左右。超过这个值,Binder 驱动直接拒绝,返回 BR_FAILED_REPLY,上层就抛异常了。
核心结论:Binder 单次事务的数据总量(包括参数、返回值、Binder 对象引用等)不能超过约 1MB。这是内核级别的硬限制。
19.2 为什么是 1MB?不是更大或更小?
你可能会问:为什么 Google 选 1MB?
我个人理解,这其实是个权衡。
- 太小了:比如 64KB,那传个稍微大点的 Bitmap 就崩了,应用开发体验极差。
- 太大了:比如 10MB,那 Binder 驱动在内核中要预留大量连续内存,对低端设备是灾难。而且大事务会长时间占用 Binder 线程,影响其他 IPC 调用。
1MB 这个值,在 Android 早期(2.x 时代)就定下来了。那时候手机内存普遍 512MB,1MB 的缓冲区已经算奢侈了。放到现在,虽然手机内存动辄 8GB、12GB,但这个限制依然没变——因为改它涉及内核、驱动、Framework 多层的兼容性,牵一发动全身。
我的经验:在项目中,如果发现需要传超过 1MB 的数据,我通常会先反思设计——是不是应该用文件描述符、ContentProvider 或者 SharedMemory 来替代?Binder 本身就不是为大数据传输设计的。
19.3 如何检测是否超限?
Binder 驱动会在事务提交时做检查。如果数据总大小超过 BINDER_VM_SIZE,驱动会直接返回错误。
在应用层,你会看到这样的异常:
android.os.TransactionTooLargeException:
data parcel size 1049088 bytes,
max allowed 1048576 bytes
注意看这个日志:它告诉你当前数据包大小是 1049088 字节,最大允许是 1048576 字节。差了 512 字节就超了。
嗯,这里要注意:这个异常是在 发送端 抛出的,不是接收端。所以如果你在 A 进程调用 B 进程的方法,数据超限了,A 进程会直接崩溃或捕获到异常。
19.4 配置方法:能改吗?怎么改?
这个问题很关键。我直接说结论:应用层无法修改这个限制。
1MB 的限制是内核驱动编译时决定的。如果你想改,必须重新编译内核。
具体步骤(以 AOSP 源码为例):
- 找到内核源码中的
drivers/android/binder.c - 修改
BINDER_VM_SIZE宏定义 - 重新编译内核并刷入设备
比如改成 2MB:
#define BINDER_VM_SIZE ((2 * 1024 * 1024) - (4096 * 2))
但说实话,我不建议你这么干。原因有三:
- 兼容性问题:系统服务(如 AMS、WMS)都假设 1MB 限制,你改了可能导致未知 bug。
- 内存压力:每个 Binder 线程都会分配这个大小的缓冲区,线程数一多,内核内存消耗很大。
- 维护成本:每次 OTA 升级都要重新编译内核,不现实。
警告:我曾经见过一个团队,为了传大文件把 BINDER_VM_SIZE 改到 8MB,结果在低端设备上频繁出现内核 OOM,最后不得不回退。改内核限制是最后的手段,不是首选方案。
19.5 实际项目中的避坑指南
既然不能改限制,那遇到大数据传输怎么办?
我总结了几种常见方案:
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 传大文件(>1MB) | 文件描述符 + mmap | 通过 Binder 传递 fd,接收方直接 mmap 读取,不经过 Binder 缓冲区 |
| 传大 Bitmap | SharedMemory | Android 10+ 推荐使用 SharedMemory API,零拷贝共享 |
| 传大量小数据 | 分片传输 | 将数据拆成多个 1MB 以内的包,逐次发送,接收方重组 |
| 传实时流数据 | Socket / Pipe | Binder 不适合流式传输,用 Unix Socket 更合适 |
我个人最常用的是 SharedMemory。它本质上是 Android 对 Linux ashmem 的封装,通过 Binder 传递一个 SharedMemory 对象,接收方可以直接映射到自己的进程空间,完全绕过了 1MB 限制。
代码示例:
// 发送端
SharedMemory sharedMemory = SharedMemory.create("my_data", 5 * 1024 * 1024); // 5MB
ByteBuffer buffer = sharedMemory.mapReadWrite();
buffer.put(largeData);
sharedMemory.unmap();
// 通过 Binder 传递 sharedMemory 对象
binder.transact(CODE_SEND_DATA, Parcel.obtain().writeSharedMemory(sharedMemory), reply, 0);
// 接收端
SharedMemory received = reply.readSharedMemory();
ByteBuffer mapped = received.mapReadOnly();
// 直接读取 mapped 中的数据
你看,这样传 5MB、10MB 都没问题,完全不受 Binder 1MB 限制的影响。
19.6 内核中的实际限制检查
如果你对底层感兴趣,可以看看 Binder 驱动中是怎么做检查的。我简单说一下关键路径:
在 binder_transaction() 函数中,驱动会计算事务的总大小:
size_t size = sizeof(struct binder_transaction_data) +
tr->data_size + tr->offsets_size;
if (size > binder_vm_size(proc)) {
return_error = BR_FAILED_REPLY;
goto err_bad_callbacks;
}
这里的 binder_vm_size(proc) 就是每个进程的 Binder 缓冲区大小,默认就是 BINDER_VM_SIZE。
注意,它检查的是 整个事务 的大小,包括协议头、数据、对象偏移表。所以即使你的数据本身只有 1000KB,加上元数据后也可能超限。
一句话总结:Binder 数据包大小限制是 1MB(实际可用约 1016KB),应用层无法修改。遇到大数据传输,请使用 SharedMemory、文件描述符或分片方案。不要试图改内核限制,那是给自己挖坑。
避坑指南:我曾经在做一个文件管理器应用时,需要跨进程传输文件列表(包含大量缩略图)。一开始直接用 Binder 传,结果频繁崩溃。后来改用 SharedMemory 传缩略图数据,Binder 只传文件路径和元数据,问题就解决了。记住:Binder 适合传控制信息,不适合传大数据。