Parcel 共享内存分析

序列化的使用场景

  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 的性能会更优;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342