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 源码为例):

  1. 找到内核源码中的 drivers/android/binder.c
  2. 修改 BINDER_VM_SIZE 宏定义
  3. 重新编译内核并刷入设备

比如改成 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 数据包大小限制与传输方案决策图 应用层发起 Binder 调用 检查:数据总大小 ≤ 1016KB? Binder 驱动正常传输 TransactionTooLargeException 大数据传输替代方案(绕过 1MB 限制) 方案一:SharedMemory 零拷贝共享内存 方案二:文件描述符 传递 fd + mmap 方案三:分片传输 拆包 < 1MB 逐次发送 注:内核限制 BINDER_VM_SIZE = 1MB - 8KB,应用层无法修改

避坑指南:我曾经在做一个文件管理器应用时,需要跨进程传输文件列表(包含大量缩略图)。一开始直接用 Binder 传,结果频繁崩溃。后来改用 SharedMemory 传缩略图数据,Binder 只传文件路径和元数据,问题就解决了。记住:Binder 适合传控制信息,不适合传大数据。

专注资料整理