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
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()
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
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()
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
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
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
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()
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()
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
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
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
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
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
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
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()
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”检查。
