美文网首页
Parcel 共享内存分析

Parcel 共享内存分析

作者: hewenyu | 来源:发表于2019-06-28 16:33 被阅读0次

    序列化的使用场景

    1. 将对象数据保存到存储设备中;
    2. 将对象数据用于网络上传输;
    3. 将对象数据用于进程之间的传输;
    4. 序列化对象的时候只是针对成员变量进行序列化,对静态成员变量,方法无法进行序列化操作;

    Serializable 和 Parcelable

    Android 开发的时候有两种序列化对象的方式 SerializableParcelable ,开发的时候两者之间还是有差异的;

    Serializable

    Serializable 是 Java 提供的一个序列化的接口,为对象提供标准的序列化和反序列化操作;通常情况下我们只需要将我们的目标类实现 Serializable 接口即可,此外可以为当前需要序列化的类指定一个 serialVersionUID 用来辅助序列化和反序列化的过程:序列化的时候系统会把当前类的 serialVersionUID 写入序列化的文件中(或其它中介), 当反序列化的时候系统回去检测文件中的 serialVersionUID 是否与当前类的 serialVersionUID 一致,如果一致, 则证明当前序列化类的版本和当前类的版本一致可以实现序列化,不一致则说明当前类和序列化的类相比发生了某些变化(例如:成员变量的数量/类型发生了变化)是无法正常发序列化的;
    通常我们在没有指定 serialVersionUID 的情况下,每次序列化的时候系统会去自动计算当前类的 hash 值作为 serialVersionUID ,此时如果当前类的内容有所改动则 hash 就会改变,既无法正常的序列化;
    我们可以手动的将当前类的 serialVersionUID 指定为 1L ,也可以用 IDE 帮我们生成对应的 hash 值作为 serialVersionUID 两者的效果是一致的;
    另外,静态成员变量属于类不属于对象,所以不会参与序列化过程,其次如果使用 transient 关键字标记的成员变量不参与序列化的过程;

    Parcelable

    Parcelable 接口是 Android 特有的接口,使用起来比 Serializable 相对复杂一点:

    1. 实现 Parcelable 接口;
    2. 实现接口中的两个方法
    // 只有在当前对象中存在文件描述符时返回 1 其它都返回 0 即可
    public int describeContents(){}
    
    // dest:该对象用来将序列化的对象写入到内存中
    // flags 只有两种值: 0 / 1 ,标志为 1 时表示当前对象需要作为返回值返回,不能立即释放资源
    // 基本上都是 0;
    public void writeToParcel(Parcel dest, @WriteFlags int flags){}
    
    1. 实例化静态内部对象 CREATOR 实现接口 Parcelable.Creator,实例化 CREATOR 时要实现其中的两个方法,其中 createFromParcel 的功能就是从Parcel中读取我们存储的对象。
    两者使用的区别

    SerializableParcelable 都能实现序列化且都可以用于 Intent 间的数据传递,但是还是存在一定的区别:

    1. Serializable 是 Java 中的序列化接口,使用起来开销量相对较大(I/O的方式),序列化和反序列化的过程需要大量的 I/O 操作。
    2. Parcelable 是 Android 中的序列化方式,用起来相对比较麻烦,但是效率高(共享内存的方式),是 Android 推荐使用的 序列化方式;
    3. Parcelable 主要用在内存序列化上,如果要将一个对象序列化到存储设备中,使用 Serializable 会是更佳的选择;

    Parcelable --> Parcel源码解析

    上面讲到了 Parcelable 使用的效率会比 Serializable 更高,接下来我们就来分析下 Parcelable 的源码来验证这句话;
    Parcelable 提供了 writeToParcel(Parcel dest, @WriteFlags int flags){} 方法并暴露了一个 Parcel 参数给开发者来操作需要缓存的数据,下面我们就来分析下 Parcel 是如何缓存数据的:

    public final class Parcel {
    
        // mNativePtr 非常的关键,该值实际上是 Native 层的 Parcel 对象的指针地址
        // 后续的数据读取/写入都是通过该指针地址来操作的
        private long mNativePtr;
        
        // 获取 Parcel 对象
        public static Parcel obtain() {
            final Parcel[] pool = sOwnedPool;
            synchronized (pool) {
                Parcel p;
                for (int i=0; i<POOL_SIZE; i++) {
                    p = pool[i];
                    if (p != null) {
                        pool[i] = null;
                        if (DEBUG_RECYCLE) {
                            p.mStack = new RuntimeException();
                        }
                        return p;
                    }
                }
            }
            // 缓存的数组中没有数据则新建一个 Parcel 对象,这里传入的参数是 0
            return new Parcel(0);
        }
        
        private Parcel(long nativePtr) {
            // 初始化Parcel
            init(nativePtr);
        }
        
        private void init(long nativePtr) {
            if (nativePtr != 0) {
                // 如果是缓存池中获取的 Parcel 对象
                mNativePtr = nativePtr;
                mOwnsNativeParcelObject = false;
            } else {
                // 传入的 nativePtr = 0
                // 调用 native 方法创建 native 层的 Native 对象,并返回其指针地址
                mNativePtr = nativeCreate();
                mOwnsNativeParcelObject = true;
            }
        }
        
        // native 方法,创建 native 层的 Parcel 对象并返回其指针地址
        private static native long nativeCreate();
        
        // 写入一个 int 类型的数据,其它 long ,String 类型的数据也是类似的调用对应的 native 方法
        public final void writeInt(int val) {
            // 调用 native 方法进行写入操作
            nativeWriteInt(mNativePtr, val);
        }
        
        // ------------------- 写入数据的 native 方法 -------------------
        
        private static native void nativeWriteInt(long nativePtr, int val);
        
        private static native void nativeWriteDouble(long nativePtr, double val);
        
        private static native void nativeWriteString(long nativePtr, String val);
        
        // ---------------------------------------------------------------
        
        // ------------------- 读取数据的 native 方法 -------------------
        
        private static native int nativeReadInt(long nativePtr);
        
        private static native double nativeReadDouble(long nativePtr);
        
        private static native String nativeReadString(long nativePtr);
        
        // --------------------------------------------------------------
    
    }
    

    上面的 Parcel 源码只显示了关键的部分,通过源码可以很清楚的看出 Parcel 对象的 创建/读/写 操作实际上都是通过调用 native 方法来实现的,看到这里好像源码已经跟不下去了,因为下面的代码就是 c/c++ 的实现了,Android Studio 中下载的 SDK 源码是不包含 native 层的代码的,因此我们需要自己去下载没有阉割版的 Android 源码;
    Parcel 对象会持有一个 mNativePtr 对象,基本上所有的native 方法都会传入该对象,注释中已经写明 mNativePtr 对象存储的实际上是 native 层的 Parcel 对象的指针地址,接下来我们深入 native 层来验证我们的这个结论:
    Parcel 对应的 JNI 代码位于:android-6.0.0_r1\frameworks\base\core\jni\android_os_Parcel.cpp

    // 这里会先对 native 方法的名称做一个映射,
    static const JNINativeMethod gParcelMethods[] = {
        // java 中的方法名称                       jni 中的方法名称
        {"nativeCreate",              "()J", (void*)android_os_Parcel_create},
        
        {"nativeWriteInt",            "(JI)V", (void*)android_os_Parcel_writeInt},
        {"nativeWriteDouble",         "(JD)V", (void*)android_os_Parcel_writeDouble},
        {"nativeWriteString",         "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeString},
        
        {"nativeReadInt",             "(J)I", (void*)android_os_Parcel_readInt},
        {"nativeReadDouble",          "(J)D", (void*)android_os_Parcel_readDouble},
        {"nativeReadString",          "(J)Ljava/lang/String;", (void*)android_os_Parcel_readString},
    }
    
    // 创建 native 的 Parcel 对象的方法,该方法在 Java 的 Parcel 对象创建 mNativePtr = 0 的时候调用
    static jlong android_os_Parcel_create(JNIEnv* env, jclass clazz)
    {
        // 创建 native 层的 Parcel 对象
        Parcel* parcel = new Parcel();
        // 获取 parcel 对象的指针地址返回给 Java 层
        // 后续数据的读/写都是通过该指针地址来操作的
        // 这里也就验证了上面说的 mNativePtr 的值是native层对象的内存地址
        return reinterpret_cast<jlong>(parcel);
    }
    
    // 写入一个 int 类型的数据
    // env:           java跟c交互的桥梁
    // clazz:       这里为 Java 层的 Parcel 的 Class 对象
    // nativePtr:    native 层 Parcel 对象的指针(内存地址)
    // val:           需要写入的值
    static void android_os_Parcel_writeInt(JNIEnv* env, jclass clazz, jlong nativePtr, jint val) {
        // 将 nativePtr 强转成 parcel 指针(实际上为Parcel对象的内存首地址)
        Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
        if (parcel != NULL) {
            // 调用native 的 parcel 对象的 writeInt32() 方法写入数据
            const status_t err = parcel->writeInt32(val);
            if (err != NO_ERROR) {
                signalExceptionForError(env, clazz, err);
            }
        }
    }
    
    // 写入一个 字符串值
    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) {  // 判空
                    // 调用native 的 parcel 对象的 writeString16() 方法写入数据
                    // 这里需要注意,除了传入字符串还传入了字符串的长度,数组作为参数传递时无法获取长度
                    err = parcel->writeString16(
                        reinterpret_cast<const char16_t*>(str),
                        env->GetStringLength(val));
                    // 释放内存
                    env->ReleaseStringCritical(val, str);
                }
            }
            ...
        }
    }
    
    // 读取一个 int 数据
    static jint android_os_Parcel_readInt(JNIEnv* env, jclass clazz, jlong nativePtr)
    {
        Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
        if (parcel != NULL) {
            // 调用native 的 parcel 对象的 readInt32() 方法读取数据
            return parcel->readInt32();
        }
        return 0;
    }
    
    // 读取一个 string 数据
    static jstring android_os_Parcel_readString(JNIEnv* env, jclass clazz, jlong nativePtr)
    {
        Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
        if (parcel != NULL) {
            size_t len;
            // 调用native 的 parcel 对象读取字符串
            const char16_t* str = parcel->readString16Inplace(&len);
            if (str) {
                return env->NewString(reinterpret_cast<const jchar*>(str), len);
            }
            return NULL;
        }
        return NULL;
    }
    

    android_os_Parcel.cpp 首先会创建一个 native 方法的映射,然后在 nativeCreate() 方法中创建一个 C++Parcel 对象然后将该对象的指针地址转成 jlong 类型的数据返回给 java 层;而数据的读取则是通过 nativeCreate() 方法中创建的 Parcel 对象来操作的,这里的 ParcelC++ 对象,对应的文件位置为: android-6.0.0_r1\frameworks\native\libs\binder\Parcel.cpp:

    
    // ----------------------------------- 写入数据 -----------------------------------
    
    // 写入一个 int 数据,parcel->writeInt32(val)
    status_t Parcel::writeInt32(int32_t val)
    {
        return writeAligned(val);
    }
    
    template<class T>
    status_t Parcel::writeAligned(T val) {
        COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));
        // 判断剩余的内存是否满足存储该数据
        if ((mDataPos+sizeof(val)) <= mDataCapacity) {
    restart_write:
            // 将指针移动到偏移的位置,然后将 val(int数据)写入到内存
            // mData: 为内存的首地址
            // mDataPos: 为指针的偏移量,例如写入了一个 int(四个字节) 数据,mDataPos 的值就会增加 四个字节,
            // 下次写入数据的时候,指针的位置就会移动到上次写入的 int 数据的内存地址后面
            *reinterpret_cast<T*>(mData+mDataPos) = val;
            // 更新内存的偏移量,这里是 int 类型的数据,因此 mDataPos 会增加 四个字节
            return finishWrite(sizeof(val));
        }
    
        status_t err = growData(sizeof(val));
        if (err == NO_ERROR) goto restart_write;
        return err;
    }
    
    // 写入一个字符串, parcel->writeString16(*str, len);
    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;
    }
    
    // ----------------------------------- 读取数据 -----------------------------------
    
    // 读取一个 int 数据: parcel->readInt32()
    int32_t Parcel::readInt32() const
    {
        return readAligned<int32_t>();
    }
    
    template<class T>
    status_t Parcel::readAligned(T *pArg) const {
        COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));
        // 判断读取的数据内存是否超出范围
        if ((mDataPos+sizeof(T)) <= mDataSize) {
            // void* 表示任意类型的数据的指针,这里会先偏移指针
            const void* data = mData+mDataPos;
            // 指针的偏移量更新,增加读取数据的大小
            mDataPos += sizeof(T);
            // 返回数据
            *pArg =  *reinterpret_cast<const T*>(data);
            return NO_ERROR;
        } else {
            return NOT_ENOUGH_DATA;
        }
    }
    
    
    // 读取一个字符串: parcel->readString16()
    String16 Parcel::readString16() const
    {
        size_t len;
        // 读取字符串
        const char16_t* str = readString16Inplace(&len);
        if (str) return String16(str, len);
        return String16();
    }
    
    const char16_t* Parcel::readString16Inplace(size_t* outLen) const
    {
        // 根据上面写入字符串的规则,这里需要先读取一个 int 类型的字符串长度(mDataPos会偏移一个int的长度)
        int32_t size = readInt32();
        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;
    }
    

    上面就是一个完整的Parcel 序列化数据的过程,接下来我们用文字来归纳一下:

    1. Java 创建一个对象实现 Parcelable 接口,重写 writeToParcel() 方法,写入调用对应的方法写入需要缓存的数据;
    2. Java 层创建 Parcel.class 对象(没有传入mNativePtr )的时候会调用 nativeCreate() 方法创建一个 native 层的Parcel.cpp 对象并返回指针地址;
    3. Parcel.cpp 会开辟一块连续的内存来缓存数据,内存的首地址是mData ,内存的偏移量是 mDataPos
    4. 写入一个数据的时候,首先会将指针移动到对应的位置 mData + mDataPos,再将数据写入到内存中,然后重新计算 mDataPos 的偏移量,mDataPos += sizeof(),下次再写入数据的时候就会跟再上次写入数据的后面;
    Parcel创建并写入数据
    1. 如果写入的数据是字符串,由于字符串的长度是不定的,需要开辟的内存大小也是未知的,因此需要先写入字符串的长度,然后根据字符串的长度计算需要开辟的内存大小,缓存字符串,因此字符串所需的最终内存大小应该是: sizeof(int) + len * sizeof(char);
    Parcel写入字符串
    1. Parcel.cpp 开辟的是一块连续的内存,根据上面的读写规则,可以得出读取数据的顺序需要和写入数据的顺序一致;
    2. Parcel 序列化数据操作的是 内存 ,而 Serializable 序列化数据操作的是 I/O ,因此,Parcel 的性能会更优;

    相关文章

      网友评论

          本文标题:Parcel 共享内存分析

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