Skip to content

大数据传输策略

追踪 Parcel Blob 的内嵌/ashmem 分支、mmap、fd ownership 和大数据失败边界。

基于android-17.0.0_r1
AndroidBinder大数据传输ParcelJava框架

大数据传输策略 ​

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

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

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 内嵌模式 ​

cpp
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模式 ​

cpp
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

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读取 ​

cpp
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

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

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。

bash
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。