大数据传输策略
Binder 大数据不是简单把 byte[] 塞进 transaction buffer。Android 17 的 native Parcel 对 Blob 设置 16 KiB 内嵌阈值;小数据写入 Parcel 本身,大数据在允许 fd 时创建 ashmem 区域,把 fd 作为对象传输,接收端重新 mmap。Java Parcel 的 writeBlob/readBlob 只是 JNI 入口,真正的模式选择、保护权限和映射释放在 native Parcel。
本文面向已经读过 Java-Parcel序列化、文件描述符传递 和 Parcel序列化详解 的读者。本文聚焦 Blob 策略、owner 和失败路径,不把 16 KiB 当作 Binder 全局 transaction 上限,也不展开共享内存通用设计。
1. Blob入口
1.1 Java API
源码文件:frameworks/base/core/java/android/os/Parcel.java
public final void writeBlob(byte[] b) {
nativeWriteBlob(mNativePtr, b, 0, b.length);
}
public final byte[] readBlob() {
return nativeReadBlob(mNativePtr);
}Java 只传入 native pointer、数组和范围;策略由 native Parcel 决定。writeBlob 适合需要 Blob wire 类型的调用者,普通 writeByteArray 仍使用普通长度前缀数组协议。
1.2 native常量
源码文件:frameworks/native/libs/binder/Parcel.cpp
static const size_t BLOB_INPLACE_LIMIT = 16 * 1024;
enum {
BLOB_INPLACE = 0,
BLOB_ASHMEM_IMMUTABLE = 1,
BLOB_ASHMEM_MUTABLE = 2,
};16 KiB 是 Blob 内嵌分支阈值,不是驱动 transaction buffer limit。Blob type 写入 Parcel,接收端据此决定直接读字节还是读取 fd。
2. 写入分支
2.1 内嵌模式
if (!mAllowFds || len <= BLOB_INPLACE_LIMIT) {
status = writeInt32(BLOB_INPLACE);
void* ptr = writeInplace(len);
if (!ptr) return NO_MEMORY;
outBlob->init(-1, ptr, len, false);
return NO_ERROR;
}只要 Parcel 禁止 fd,或者长度不超过 16 KiB,就内嵌写入。内嵌 Blob 没有 fd,Blob 对象只引用 Parcel 数据区,不能在 Parcel 生命周期之外继续使用指针。
2.2 ashmem模式
int fd = ashmem_create_region("Parcel Blob", len);
int result = ashmem_set_prot_region(fd,
PROT_READ | PROT_WRITE);
void* ptr = mmap(nullptr, len,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (!mutableCopy) {
result = ashmem_set_prot_region(fd, PROT_READ);
}
status = writeInt32(
mutableCopy ? BLOB_ASHMEM_MUTABLE
: BLOB_ASHMEM_IMMUTABLE);
status = writeFileDescriptor(fd, true);
outBlob->init(fd, ptr, len, mutableCopy);大 Blob 先创建 ashmem、设置读写保护、mmap;不可变副本在写入 fd 前降为只读。Parcel 接管 fd ownership,失败路径 munmap/close;任何创建、保护、映射或写 fd 失败都返回错误。
2.3 mAllowFds
大数据不一定走 ashmem:如果 pushAllowFds(false) 使 mAllowFds=false,即使长度很大也必须内嵌,空间不足时返回 NO_MEMORY。禁止 fd 是安全/协议约束,不是性能优化开关。
3. 读取分支
3.1 内嵌读取
源码文件:frameworks/native/libs/binder/Parcel.cpp
if (blobType == BLOB_INPLACE) {
const void* ptr = readInplace(len);
if (!ptr) return BAD_VALUE;
outBlob->init(-1, const_cast<void*>(ptr),
len, false);
return NO_ERROR;
}读取内嵌 Blob 只返回 Parcel 数据区指针;调用者必须保证 Blob 不超过 Parcel 生命周期。
3.2 ashmem读取
bool isMutable = blobType == BLOB_ASHMEM_MUTABLE;
int fd = readFileDescriptor();
if (!ashmem_valid(fd)) return BAD_VALUE;
int size = ashmem_get_size_region(fd);
if (size < 0 || size_t(size) < len) return BAD_VALUE;
void* ptr = mmap(nullptr, len,
isMutable ? PROT_READ | PROT_WRITE : PROT_READ,
MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) return NO_MEMORY;
outBlob->init(fd, ptr, len, isMutable);
return NO_ERROR;接收端检查 fd 是否 ashmem、区域大小是否至少满足请求长度,再按 mutable 标记设置 mmap 权限。fd size 不足或 mmap 失败不会返回可用 Blob。
3.3 Blob释放
源码文件:frameworks/native/libs/binder/Parcel.cpp
Parcel::Blob::~Blob() { release(); }
void Parcel::Blob::release() {
if (mFd != -1 && mData) munmap(mData, mSize);
clear();
}Blob 释放负责 munmap;fd ownership/Parcel object cleanup 还由 Parcel 对象表和 fd 释放路径管理。调用者应尽快释放 Blob,不要把 mmap 指针当作长期独立内存。
4. Java边界
4.1 JNI转换
源码文件:frameworks/base/core/jni/android_os_Parcel.cpp
static void android_os_Parcel_writeBlob(
JNIEnv* env, jlong nativePtr,
jobject data, jint offset, jint length) {
Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
status_t err = parcel->writeBlob(
length, false, &blob);
if (err != NO_ERROR) signalExceptionForError(env, err);
}JNI 将 Java byte[] 的范围传给 native Blob;NO_MEMORY、FDS_NOT_ALLOWED 等 status 通过 Java 异常映射。Blob 的 native mapping 由 Java/native wrapper 生命周期共同管理。
4.2 普通数组对照
writeByteArray/readByteArray 走普通数据区和长度校验;writeBlob/readBlob 允许 fd-backed 共享区域。不能只把 writeBlob 当成“更大的 byte array”。
5. 失败路径
ashmem 创建失败返回 NO_MEMORY;保护、mmap、写 fd 失败会清理映射和 fd。接收端无效 ashmem、大小不足和 mmap 失败分别产生 BAD_VALUE/NO_MEMORY。BINDER_DISABLE_BLOB 构建直接返回 INVALID_OPERATION。
大 Blob 仍受 fd 数量、进程地址空间、驱动对象传输和 receiver 生命周期限制;ashmem 不是绕过所有 Binder 资源限制的万能通道。
6. 测试边界
源码文件:frameworks/native/libs/binder/tests/binderParcelUnitTest.cpp
测试覆盖 Parcel Blob 的构造/读取、fd 约束和对象 metadata;Java ParcelTest 覆盖 Parcel API 的 byte array、fd 与回收。若具体测试只在支持 ashmem 的目标构建运行,不能外推到 BINDER_DISABLE_BLOB 或非 Linux backend。
rg -n "writeBlob|readBlob|BLOB_INPLACE_LIMIT|BLOB_ASHMEM" frameworks/native/libs/binder/Parcel.cpp frameworks/base/core/java/android/os/Parcel.java
rg -n "nativeWriteBlob|nativeReadBlob|writeBlob" frameworks/base/core/jni/android_os_Parcel.cpp
rg -n "Blob|hasFileDescriptors|writeByteArray" frameworks/native/libs/binder/tests/binderParcelUnitTest.cpp frameworks/base/core/tests/coretests/src/android/os/ParcelTest.java排查大数据事务失败时,先看 writeBlob 的 allow-fd/长度分支,再看 ashmem 创建/protection/mmap、fd 对象传输和接收端 size 校验;不要把单次 byte[] 长度与真实 Binder transaction 上限混为一谈。
7. 后续专题边界
本文只回答 Parcel::writeBlob() 如何在内嵌数据和 ashmem-backed fd 之间选择,以及两种结果由谁释放。它不承担以下主题:
- Binder 驱动为什么采用一次目标 buffer 拷贝,以及对象翻译发生在哪个阶段;该机制属于驱动 transaction/buffer 专题。
TransactionTooLargeException如何由调用端统计、驱动失败和 Java 异常映射共同形成;该问题属于异常诊断专题。- 应用或系统服务何时应改用共享内存、流式 fd、文件或分片协议;该问题属于跨进程大数据设计专题。
因此,writeBlob() 是一种具体 wire-format 分流,不是所有大数据 Binder API 的默认实现,也不是对 transaction size 错误的通用修复。读者在源码中没有看到调用方使用 writeBlob() 时,不应根据 payload 较大就推断它自动走 ashmem。
