Skip to content

Parcel读写契约

从对齐、游标、长度前缀、接口请求头、对象保护区、异常 reply 和外部 buffer 所有权理解 Parcel 的真实读写契约。

基于android-17.0.0_r1
AndroidBinderParcel序列化源码阅读

Parcel读写契约 ​

Parcel 不是一种可以脱离 Android 版本长期保存的通用序列化格式。它是 Binder 调用两端共享的 有状态读写容器:写方按照既定顺序推进游标,读方必须以兼容顺序消费;Binder 对象所在区域不能 被普通整数读取跨越;请求头和 reply 头还携带接口、StrictMode、WorkSource 和异常状态。

本文面向已经读过 Binder数据转换链 和 Binder对象编码 的读者。前两篇解释 data/offsets 如何进入驱动, 本文只研究 Parcel 自身:标量如何对齐,字符串如何带长度,dataPosition 如何影响读写,容量 如何增长,接口 token 如何被校验,尾部未消费数据如何暴露版本错配,异常如何进入 reply,以及 外部 Binder buffer 如何从“借用”转成“自有”。

本文不逐项枚举 Java Parcel API,也不展开 Parcelable 反射、Bundle lazy value 和 AIDL 代码生成。

1. 三个游标 ​

Size与position ​

Parcel 至少要区分:

状态含义
dataPosition下一次读写开始位置
dataSize当前有效数据末尾
dataCapacity已分配内存容量

写入会推进 position,必要时扩大 size;读取消费 position,但不改变 size;capacity 只是内存 空间,不代表其中全部字节有效。

源码文件:frameworks/native/libs/binder/Parcel.cpp

cpp
size_t Parcel::dataPosition() const {
    return mDataPos;
}

size_t Parcel::dataCapacity() const {
    return mDataCapacity;
}

void Parcel::setDataPosition(size_t pos) const {
    if (pos > INT32_MAX)
        LOG_ALWAYS_FATAL("pos too big: %zu", pos);

    mDataPos = pos;
    if (auto* fields = maybeKernelFields()) {
        fields->mNextObjectHint = 0;
        fields->mObjectsSorted = false;
    }
}

重设 position 还会重置对象读取 hint。它不是普通数组下标赋值:之后所有类型解析、对象保护和 request header 读取都从新位置开始。

写后回读 ​

忘记 setDataPosition(0) 时,读取从数据末尾开始,通常得到 NOT_ENOUGH_DATA 或默认值,而不是 自动回到开头。

2. 对齐标量 ​

写入模板 ​

源码文件:frameworks/native/libs/binder/Parcel.cpp

相关函数:writeAligned()、finishWrite()

cpp
template<class T>
status_t Parcel::writeAligned(T value) {
    static_assert(
            PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));
    static_assert(std::is_trivially_copyable_v<T>);

    if (mDataPos + sizeof(value) <= mDataCapacity) {
restart_write:
        status_t status =
                validateReadData(
                        mDataPos + sizeof(value));
        if (status != OK)
            return status;

        memcpy(mData + mDataPos,
                &value, sizeof(value));
        return finishWrite(sizeof(value));
    }

    status_t err = growData(sizeof(value));
    if (err == NO_ERROR)
        goto restart_write;
    return err;
}

标量必须满足 Parcel 对齐约束;写入前还会调用 validateReadData,防止覆盖现有 Binder object 区域。finishWrite 推进 position,并在超过旧 size 时更新有效数据长度。

读取模板 ​

源码文件:frameworks/native/libs/binder/Parcel.cpp

cpp
template<class T>
status_t Parcel::readAligned(T* value) const {
    static_assert(
            PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));
    static_assert(std::is_trivially_copyable_v<T>);

    if (mDataPos + sizeof(T) <= mDataSize) {
        if (objectsCount() > 0) {
            status_t err =
                    validateReadData(
                            mDataPos + sizeof(T));
            if (err != NO_ERROR) {
                mDataPos += sizeof(T);
                return err;
            }
        }

        memcpy(value, mData + mDataPos,
                sizeof(T));
        mDataPos += sizeof(T);
        return NO_ERROR;
    }
    return NOT_ENOUGH_DATA;
}

读取越界返回 NOT_ENOUGH_DATA;跨越对象区域返回 PERMISSION_DENIED。即使对象校验失败,源码仍 推进期望长度,因此失败后的 position 也发生变化,调用方不应无条件继续按旧位置解析。

3. 长度前缀 ​

String16读取 ​

源码文件:frameworks/native/libs/binder/Parcel.cpp

相关函数:readString16Inplace()

cpp
const char16_t* Parcel::readString16Inplace(
        size_t* outLen) const {
    int32_t size = readInt32();
    if (size >= 0 && size < INT32_MAX) {
        *outLen = size;
        const char16_t* str =
                static_cast<const char16_t*>(
                        readInplace(
                                (size + 1)
                                * sizeof(char16_t)));
        if (str != nullptr &&
                str[size] == u'\0') {
            return str;
        }
    }
    *outLen = 0;
    return nullptr;
}

String16 的 wire 形态是长度加包含终止符的 UTF-16 内容,并按 Parcel 规则推进游标。负长度可以 表达 null;长度溢出、数据不足或终止符缺失会返回空指针。

String8读取 ​

源码文件:frameworks/native/libs/binder/Parcel.cpp

cpp
const char* Parcel::readString8Inplace(
        size_t* outLen) const {
    int32_t size = readInt32();
    if (size >= 0 && size < INT32_MAX) {
        *outLen = size;
        const char* str =
                static_cast<const char*>(
                        readInplace(size + 1));
        if (str != nullptr &&
                str[size] == '\0') {
            return str;
        }
    }
    *outLen = 0;
    return nullptr;
}

String8 与 String16 的字符宽度不同,但都有长度、终止符和边界校验。把 String8 写入后用 readString16 读取,不会自动做类型协商;必须使用对应的 UTF 转换 API。

4. 容量增长 ​

growData ​

源码文件:frameworks/native/libs/binder/Parcel.cpp

cpp
status_t Parcel::growData(size_t len) {
    if (len > INT32_MAX)
        return BAD_VALUE;

    if (mDataPos > mDataSize)
        return BAD_VALUE;

    if (len > SIZE_MAX - mDataSize)
        return NO_MEMORY;

    size_t newSize =
            ((mDataSize + len) * 3) / 2;
    return newSize <= mDataSize
            ? NO_MEMORY
            : continueWrite(
                    std::max(newSize,
                             static_cast<size_t>(128)));
}

增长策略是实现细节,不是稳定 ABI。更重要的不变量是:growData 只预期在数据末尾增长,且进行 整数溢出检查。

continueWrite ​

源码文件:frameworks/native/libs/binder/Parcel.cpp

cpp
status_t Parcel::continueWrite(size_t desired) {
    if (desired > INT32_MAX)
        return BAD_VALUE;

    size_t objectsSize =
            kernelFields
            ? kernelFields->mObjectsSize
            : rpcFields->mObjectPositions.size();

    if (desired < mDataSize) {
        while (objectsSize > 0 &&
                kernelFields->mObjects[
                        objectsSize - 1]
                + sizeof(flat_binder_object)
                > desired) {
            objectsSize--;
        }
    }

    if (mOwner) {
        uint8_t* data =
                static_cast<uint8_t*>(
                        malloc(desired));
        /* 从外部 owner 转为自有内存 */
    } else if (mData) {
        /* release 被截断对象,再 realloc */
    } else {
        mData = static_cast<uint8_t*>(
                malloc(desired));
        mDataSize = mDataPos = 0;
        mDataCapacity = desired;
    }
    return NO_ERROR;
}

缩容会先移除超出新范围的 object offsets,并释放对应 Binder/fd 引用;外部 owner Parcel 若要 继续写入,需要复制为自有内存。容量变化不仅是 realloc,还涉及对象所有权迁移。

5. 对象保护 ​

普通读写限制 ​

源码文件:frameworks/native/libs/binder/Parcel.cpp

相关函数:validateReadData()

cpp
if (fields->mNextObjectHint <
        fields->mObjectsSize &&
        upperBound >
        fields->mObjects[
                fields->mNextObjectHint]) {
    size_t nextObject =
            fields->mNextObjectHint;
    do {
        if (mDataPos <
                fields->mObjects[nextObject]
                + sizeof(flat_binder_object)) {
            return PERMISSION_DENIED;
        }
        nextObject++;
    } while (nextObject <
            fields->mObjectsSize &&
            upperBound >
            fields->mObjects[nextObject]);
    fields->mNextObjectHint = nextObject;
}

普通 int/string 读写不能横跨 object 区域;Binder 对象必须走 readStrongBinder/readObject 等专用 入口。objects 失序时,源码会先排序再检查。

外部视图 ​

源码文件:frameworks/native/libs/binder/Parcel.cpp

相关函数:ipcSetDataReference()

cpp
mData = const_cast<uint8_t*>(data);
mDataSize = mDataCapacity = dataSize;
fields->mObjects =
        const_cast<binder_size_t*>(objects);
fields->mObjectsSize =
        fields->mObjectsCapacity = objectsCount;
mOwner = releaseFunction;

接收事务的 Parcel 借用驱动映射 buffer,mOwner 保存释放函数。它最初不拥有 data;继续写入或 重设大小时,continueWrite 才可能复制为自有内存。

6. 请求头 ​

请求头 ​

源码文件:frameworks/native/libs/binder/Parcel.cpp

cpp
status_t Parcel::writeInterfaceToken(
        const char16_t* str, size_t len) {
    if (maybeKernelFields()) {
        const IPCThreadState* threadState =
                IPCThreadState::self();
        writeInt32(
                threadState->getStrictModePolicy()
                | STRICT_MODE_PENALTY_GATHER);
        updateWorkSourceRequestHeaderPosition();
        writeInt32(
                threadState->shouldPropagateWorkSource()
                ? threadState
                        ->getCallingWorkSourceUid()
                : IPCThreadState::kUnsetWorkSource);
        writeInt32(kHeader);
    }
    return writeString16(str, len);
}

接口 token 前还有 StrictMode、WorkSource 和 libbinder header。把请求头理解为“只写 descriptor” 会导致手写 Stub/Proxy 的 dataPosition 错位。

enforceInterface ​

源码文件:frameworks/native/libs/binder/Parcel.cpp

cpp
int32_t strictPolicy = readInt32();
if ((threadState
        ->getLastTransactionBinderFlags()
        & IBinder::FLAG_ONEWAY) != 0) {
    threadState->setStrictModePolicy(0);
} else {
    threadState->setStrictModePolicy(
            strictPolicy);
}

updateWorkSourceRequestHeaderPosition();
int32_t workSource = readInt32();
threadState
        ->setCallingWorkSourceUidWithoutPropagation(
                workSource);

int32_t header = readInt32();
if (header != kHeader &&
        !mServiceFuzzing)
    return false;

const char16_t* token =
        readString16Inplace(&tokenLength);
return tokenLength == expectedLength &&
        !memcmp(token, expected,
                tokenLength * sizeof(char16_t));

enforceInterface 同时消费请求 header 并设置当前 IPCThreadState 上下文。descriptor 不匹配返回 false, Java 层封装会抛 SecurityException。

7. 尾部检查 ​

Java边界 ​

源码文件:frameworks/base/core/java/android/os/Parcel.java

java
public void enforceNoDataAvail() {
    final int unread = dataAvail();
    if (unread > 0) {
        throw new BadParcelableException(
                "Parcel data not fully consumed, unread size: "
                + unread);
    }
}

AIDL Stub 在读完所有声明参数后调用它,可以捕获客户端多写字段、服务端少读字段或版本不兼容。 这不是格式美化,而是阻止隐藏尾部数据被忽略的安全边界。

Native边界 ​

源码文件:frameworks/native/libs/binder/Parcel.cpp

相关函数:enforceNoDataAvail()

Native 版本返回 binder::Status;有未读字节时使用 EX_BAD_PARCELABLE。Java 和 native 的错误表示 不同,但证明目标相同:方法参数必须完整消费。

8. Reply头 ​

成功状态 ​

源码文件:frameworks/base/core/java/android/os/Parcel.java

java
public final void writeNoException() {
    AppOpsManager
            .prefixParcelWithAppOpsIfNeeded(this);

    if (StrictMode.hasGatheredViolations()) {
        writeInt(
                EX_HAS_STRICTMODE_REPLY_HEADER);
        final int sizePosition =
                dataPosition();
        writeInt(0);
        StrictMode
                .writeGatheredViolationsToParcel(
                        this);
        final int payloadPosition =
                dataPosition();
        setDataPosition(sizePosition);
        writeInt(
                payloadPosition - sizePosition);
        setDataPosition(payloadPosition);
    } else {
        writeInt(0);
    }
}

writeNoException 实际写的是 RPC response header;成功常见值为 0,也可能带 StrictMode reply header。 调用方必须先 readException,再读业务返回值。

异常状态 ​

源码文件:frameworks/base/core/java/android/os/Parcel.java

java
public final void writeException(
        @NonNull Exception exception) {
    int code = getExceptionCode(exception);
    writeInt(code);
    if (code == 0)
        throw new RuntimeException(exception);

    writeString(exception.getMessage());
    writeInt(0);

    if (code == EX_SERVICE_SPECIFIC) {
        writeInt(
                ((ServiceSpecificException)
                        exception).errorCode);
    }
}

Java 只支持一组可映射的异常类型;未知 checked exception 会转为 RuntimeException,而不是任意 异常对象跨进程复制。

读取异常 ​

源码文件:frameworks/base/core/java/android/os/Parcel.java

java
public final void readException() {
    int code = readExceptionCode();
    if (code != 0) {
        String message = readString();
        readException(code, message);
    }
}

调用方若跳过 readException,后续第一个业务 read 可能把 exception code 当成返回值,造成难以定位 的数据错位。

9. Java桥接 ​

Java Parcel 的 setDataPosition、writeInterfaceToken、readString 和 exception API 最终进入 native Parcel 或围绕 native 游标读写。Java 层还提供 ReadWriteHelper、Parcelable、TypedObject 和数组 分配限制;它们不改变底层“顺序、游标、长度和对象边界”四个不变量。

10. 测试输入 ​

字符串边界 ​

源码文件:frameworks/native/libs/binder/tests/binderParcelUnitTest.cpp

相关测试:NonNullTerminatedString8()、NonNullTerminatedString16()、Utf8FromUtf16Read()、 Utf8AsUtf16Write()

测试写入缺少终止符或使用 UTF 转换 API,重置 position 后读取,断言非法终止符失败、合法转换保持 内容和最终 position。它们证明长度与终止符都会被验证。

尾部数据 ​

源码文件:frameworks/native/libs/binder/tests/binderParcelUnitTest.cpp

相关测试:EnforceNoDataAvail()

cpp
Parcel parcel;
parcel.writeInt32(kTestInt);
parcel.writeString8(kTestString);
parcel.setDataPosition(0);

EXPECT_EQ(kTestInt, parcel.readInt32());
EXPECT_EQ(
        parcel.enforceNoDataAvail()
                .exceptionCode(),
        Status::Exception::EX_BAD_PARCELABLE);

EXPECT_EQ(kTestString,
        parcel.readString8());
EXPECT_EQ(
        parcel.enforceNoDataAvail()
                .exceptionCode(),
        Status::Exception::EX_NONE);

输入包含整数和字符串;只读整数时断言还有尾部数据,继续读字符串后断言全部消费。它能证明参数 完整性,不证明业务字段语义正确。

对象边界 ​

WriteObjectUnsortedThenValidate() 在 offsets 失序时尝试从 Binder object 中间读 int64,并断言 PERMISSION_DENIED;AppendOverObject() 尝试覆盖 fd object,断言失败。它们证明普通读写不能越过 对象区。

Java请求头 ​

源码文件:frameworks/base/core/tests/coretests/src/android/os/ParcelTest.java

相关测试:testCallingWorkSourceUidAfterWrite()、testCallingWorkSourceUidAfterEnforce()、 testParcelWithMultipleHeaders()、testStrings()

测试在 writeInterfaceToken 前后替换 WorkSource,重置 position 后 enforce,并断言首个 request header 拥有稳定位置;字符串测试覆盖 null、空串、嵌入 NUL 和多语言文本。它们不能证明所有 AIDL 参数类型 的兼容性。

11. 失败边界 ​

顺序错位 ​

写 int/string/binder,读 string/int/binder 时没有自描述 schema 帮助恢复。错误可能表现为长度异常、 NOT_ENOUGH_DATA、BAD_TYPE 或后续字段错位。

游标错位 ​

未重置 position、异常后继续读取、临时回填长度后未恢复 payloadPosition,都会让后续读写从错误位置 开始。源码中的 sizePosition/payloadPosition 模式是长度回填的标准做法。

Owner变化 ​

接收 Parcel 的 mOwner 指向 Binder buffer 释放函数;一旦继续写入,它可能复制为自有内存。调试 buffer 生命周期时必须确认当前 Parcel 是 borrowed 还是 owned。

版本差异 ​

Parcel 不是稳定磁盘格式。字段顺序、request/reply header 和 Parcelable 实现都可能随版本变化;跨 版本协议应使用 Stable AIDL、显式版本字段或其他稳定编码。

12. 阅读边界 ​

游标与Owner ​

dataPosition 是消费者游标,dataSize 是已提交内容边界,capacity 只是可写空间;扩容不会自动改变 消费者语义。对象 offsets 和外部视图还有自己的 owner,不能用一个游标解释所有 Parcel 状态。

本文证明了 Parcel 的游标、对齐、长度前缀、容量增长、对象保护、请求 header、尾部检查、异常 reply 和外部 buffer owner。没有展开完整 Java Parcel API、Parcelable/Bundle lazy decode、AIDL 生成代码、 RPC Parcel、blob/ashmem 优化或兼容性框架。

继续阅读 Binder对象编码 复习对象区,再进入 BD010 跨进程引用计数。 排查序列化问题时按“写入顺序、position、长度、对象 offset、header、尾部、reply exception”检查。