Android开发Android开发经验谈程序员

进程间通信—— Parcel

2018-04-15  本文已影响104人  某昆

本文主要内容

Parcel简介

Parcel的英文直译是打包,是对进程间通信数据传递的形象描述。进程间通信数据传递,无法直接传递数据的内存地址,因为进程保护,无法获取另一进程中的内存数据。

但把对象在进程A中占据的内存相关数据打包起来,然后传递到进程B中,由B在自己进程空间中复制这个对象,这是可行的。

Parcel就具备这种打包和重组的能力。

Parcel能传递的数据有:

值得一提的是,Binder和FileDescriptor被称为Active Objects,从Parcel中读取的,并不是重新创建的对象实例,而是原来那个被写入的实例(或者是特殊代理实现)

注意,Parcel写入方和读取方所使用的协议必须是完全一致的

Parcel类介绍

Parcel提供java端接口,方便开发者调用,但它的实现是c++,它涉及到的类有:

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数据顺序存储的,而且有指针位置,可以直接读取对应的值,不需要传入额外的参数。

上一篇下一篇

猜你喜欢

热点阅读