进程间通信—— Parcel
本文主要内容
- Parcel简介
- Parcel类介绍
- 字符串读写
Parcel简介
Parcel的英文直译是打包,是对进程间通信数据传递的形象描述。进程间通信数据传递,无法直接传递数据的内存地址,因为进程保护,无法获取另一进程中的内存数据。
但把对象在进程A中占据的内存相关数据打包起来,然后传递到进程B中,由B在自己进程空间中复制这个对象,这是可行的。
Parcel就具备这种打包和重组的能力。
Parcel能传递的数据有:
- 原始数据,比如基础数据类型和String类型
- 实现Parcelables接口的对象
- Binder
- FileDescriptor
值得一提的是,Binder和FileDescriptor被称为Active Objects,从Parcel中读取的,并不是重新创建的对象实例,而是原来那个被写入的实例(或者是特殊代理实现)
注意,Parcel写入方和读取方所使用的协议必须是完全一致的
Parcel类介绍
Parcel提供java端接口,方便开发者调用,但它的实现是c++,它涉及到的类有:
- Parcel.java
- Parcel.h
- Parcel.cpp
Parcel.h中定义了相关指针及指针位置,以便于存储数据。
uint8_t* mData;
size_t mDataSize;
size_t mDataCapacity;
mutable size_t mDataPos;
mData即是数据存储的指针,mDataSize是数据存在量的大小,mDataCapacity是数据存储的最大值,mDataPos则是数据存储的指针位置。
另外,java端的Parcel与c++中的Parcel的对应方式也很有意思。
private void init(long nativePtr) {
if (nativePtr != 0) {
mNativePtr = nativePtr;
mOwnsNativeParcelObject = false;
} else {
mNativePtr = nativeCreate();
mOwnsNativeParcelObject = true;
}
}
nativeCreate是一个native方法,其实现为:
static jlong android_os_Parcel_create(JNIEnv* env, jclass clazz)
{
Parcel* parcel = new Parcel();
return reinterpret_cast<jlong>(parcel);
}
c++中将指针转化为long数据,并且返回给java端,后续java端再利用返回的long数据,找到对应的Parcel c++对象。
当java端调用方法时,将mNativePtr作为参数传入,c++端则能通过此long值找到对应的对象。
Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
字符串读写
先来看写字符串的情况:
Java端代码:
public void writeString(Parcel p, String s) {
nativeWriteString(p.mNativePtr, s);
}
对应的JNI方法为:
static void android_os_Parcel_writeString(JNIEnv* env, jclass clazz, jlong nativePtr, jstring val)
{
Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
if (parcel != NULL) {
status_t err = NO_MEMORY;
if (val) {
const jchar* str = env->GetStringCritical(val, 0);
if (str) {
err = parcel->writeString16(
reinterpret_cast<const char16_t*>(str),
env->GetStringLength(val));
env->ReleaseStringCritical(val, str);
}
} else {
err = parcel->writeString16(NULL, 0);
}
if (err != NO_ERROR) {
signalExceptionForError(env, clazz, err);
}
}
}
在JNI方法中,先找到c++端的Parcel对象,env获取string值,然后调用writeString16方法,env最终释放string,完成整个写字符串。
status_t Parcel::writeString16(const char16_t* str, size_t len)
{
if (str == NULL) return writeInt32(-1);
status_t err = writeInt32(len);
if (err == NO_ERROR) {
len *= sizeof(char16_t);
uint8_t* data = (uint8_t*)writeInplace(len+sizeof(char16_t));
if (data) {
memcpy(data, str, len);
*reinterpret_cast<char16_t*>(data+len) = 0;
return NO_ERROR;
}
err = mError;
}
return err;
}
writeString16方法中先写入长度值,writeInplace这个方法比较有意思,为了保证写入的长度一定是4的位数,需要进行补位操作,补位就在这个方法中。最后调用memcpy方法复制字符串到mData中,完成写字符串。
特别注意下,字符串写完后还往内存中写了一个数值0。
具体的补位操作在writeInplace方法中,不详细介绍了,比较简单。
接下来介绍读字符串:
static jstring android_os_Parcel_readString(JNIEnv* env, jclass clazz, jlong nativePtr)
{
Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
if (parcel != NULL) {
size_t len;
const char16_t* str = parcel->readString16Inplace(&len);
if (str) {
return env->NewString(reinterpret_cast<const jchar*>(str), len);
}
return NULL;
}
return NULL;
}
同样是通过转换获取Parcel对象,然后调用readString16Inplace方法。
const char16_t* Parcel::readString16Inplace(size_t* outLen) const
{
int32_t size = readInt32();
// watch for potential int overflow from size+1
if (size >= 0 && size < INT32_MAX) {
*outLen = size;
const char16_t* str = (const char16_t*)readInplace((size+1)*sizeof(char16_t));
if (str != NULL) {
return str;
}
}
*outLen = 0;
return NULL;
}
因为写字符串的时候将字符串长度写入,所以在上述方法中能读出字符串的长度。
因为写入的时候,最后写了一个零,所以readInplace方法中传入的长度参数为 len+1 。
const void* Parcel::readInplace(size_t len) const
{
if (len > INT32_MAX) {
// don't accept size_t values which may have come from an
// inadvertent conversion from a negative int.
return NULL;
}
if ((mDataPos+pad_size(len)) >= mDataPos && (mDataPos+pad_size(len)) <= mDataSize
&& len <= pad_size(len)) {
const void* data = mData+mDataPos;
mDataPos += pad_size(len);
ALOGV("readInplace Setting data pos of %p to %zu", this, mDataPos);
return data;
}
return NULL;
}
最后根据长度以及补位相关,计算得到指针位置mDataPos,并返回指针,完成整个读取。
和前文说的一样,可见 Parcel写入方和读取方所使用的协议必须是完全一致的,因为Parcel数据顺序存储的,而且有指针位置,可以直接读取对应的值,不需要传入额外的参数。