进程间通信—— Parcel

作者: 某昆 | 来源:发表于2018-04-15 19:44 被阅读104次

    本文主要内容

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

    相关文章

      网友评论

        本文标题:进程间通信—— Parcel

        本文链接:https://www.haomeiwen.com/subject/vqgocftx.html