Parcelable与Serializable区别

1 概述

首先,本篇内容的分析以及涉及到的Android源码都是基于android-6.0.1_r1分支的。

把对象转化为字节序列的过程叫序列化,反之把字节序列恢复成对象叫反序列化。Parcelable对象主要用于内存变量,是为了Android不同组件间高效传输数据而设计的,而Serializable的作用是为了保存对象的属性到本地文件,数据库,网络流,RMI以方便数据传输,因为其序列化使用了反射,且会生成大量的临时变量(会造成频繁的GC),故效率要相对慢。

`Serializable`是Java的一个标志接口。其是一个空接口,内容如下

```
public interface Serializable {
}
```
其通常用法是,实现`Serializable`接口,通过`ObjectInputStream`和`ObjectOutputStream`进行对象的读写。

2 Serializable

Serializable序列化浅析,参考了Java序列化高级认识

  1. Serializable序列化ID

    虚拟机是否允许反序列化,不仅取决于类的路径和代码功能是否一致,一个非常重要的要点是:两个类的序列化ID是否一致,也就是 private statiac final long serialVersionUID = 1L ,该值的存在是为了限制一些对序列化文件的访问,默认设置为1L就好。

  2. 静态变量的Serializable序列化

    序列化并不保存静态变量的,因为序列化保存的是对象的状态,而静态变量属于类的状态。

  3. 父类的序列化

    若父类未实现Serializable,子类实现了Serializable接口,序列化子类的时候,虚拟机是不会序列化父类的,但是在反序列化子类的时候,为了构造父类,必须调用父类的无参构造函数(父类必须提供无参构造函数),来先创建父类,再创建子类,此时父类的对象被初始化为初始值(如int的0,对象的null等). 当然,可以考虑在无参构造函数中,对反序列化的父类变量进行初始化。

  4. Transient关键字

    Transient可以使修饰的字段不被序列化到文件中,反序列化的时候,该变量的值被设定为初始值(如int的0,对象的null等)。当然,可以利用1.3中,把不需要初始化的属性放在父类,父类不实现Serializable接口,达到同样的效果。

  5. 对敏感字段加密

    在序列化过程中,虚拟机会试图调用对象类里面的writeObjectreadObject方法,进行用户自定义的序列化和反序列化。如果没有这样的方法,则调用默认的ObjectOutputStreamdefaultWriteObject方法和ObjectInputStreamdefaultReadObject方法。基于这个原理,可以实现对敏感字段加密。

  6. 序列化存储规则

    Java序列化机制为了节省磁盘空间,当写入文件的为同一对象时,并不会再将内容进行存储,只是再次存储一份引用,反序列化的时候恢复引用关系,使这两个引用指向同一个对象。此处有一点需要注意的是:即使在第二次写入这个对象之前,改变了对象中属性的值,仍然只是保存一个对象,反序列化的时候读取到的值,也只是第一次写入的值。

3 Parcelable

Parcelable是Android考虑Serializable效率不高而自己开发的一套序列化机制。用于Bundle(Bundle实现了Parcelable接口)传值,AIDL通讯等方面。Parcelable是IBinder通讯的消息载体。

3.1接口定义

public interface Parcelable {
    public static final int PARCELABLE_WRITE_RETURN_VALUE = 0x0001;
    public static final int CONTENTS_FILE_DESCRIPTOR = 0x0001;
    //内容描述接口,返回一个掩码,表示一组特殊对象类型的Parcelable。
    public int describeContents();
    //写入函数接口
    public void writeToParcel(Parcel dest, int flags);
    //读取接口,目的是从Parcel中构造一个实现了Pacelable的类的实例。
    //这里用到了模板方法,继承类名通过模板参数传入,为能够实现模板参数传入,这里定义了Creator嵌入接口,内含两个函数分别返回单个和多个继承实例。
    public interface Creator<T> {                   public T createFromParcel(Parcel source);
        public T[] newArray(int size);
    }
    public interface ClassLoaderCreator<T> extends Creator<T> {
        public T createFromParcel(Parcel source, ClassLoader loader);
    }
}

3.2读写规范

必须实现Creator接口,CREATOR用于从Parcel中实例化你的包装类。该接口有两个方法:
    createFromParcel:实现从in中创建出类的实例的功能。
    newArray:创建类的实例数组。
在读取Parcelable outin对应的属性顺序不能错,否则取不到值。
ClassLoaderCreator<T> : 专业化的Creator,允许你接收的Object内部创建的ClassLoader对象。
writeToParcel(Parcel dest, int flags):

4 Parcel源码实现

Parcel文件位于`frameworks/base/core/java/android/os/Parcel.java,应用程序可以通过Parcel.obtain()获取一个Parcel对象。

private static final int POOL_SIZE = 6;
private static final Parcel[] sOwnedPool = new Parcel[POOL_SIZE];

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) {//引用不为null表示可用
                    //引用置为null,这样下次就知道这个Parcel被占用了。
                pool[i] = null;
                return p;
            }
        }
    }
    return new Parcel(0);
}

这里,获取对象的时候,首先是是从系统默认的大小为6的sOwnedPool中查询有没有未使用的,若是池中有多余的则直接使用,并且将其使用标志置为null,表示已被占用。否则若没有多余的则调用new Parcel(0) 创建一个新的Parcel。

以下是创建Parcel的构造方法,参数nativePtr = 0,最终调用了Native层的nativeCreate方法。

private Parcel(long nativePtr) {
    //Log.i(TAG, "Initializing obj=0x" + Integer.toHexString(obj), mStack);
    init(nativePtr);
}

private void init(long nativePtr) {
    if (nativePtr != 0) {
        mNativePtr = nativePtr;
        mOwnsNativeParcelObject = false;
    } else {    //传值为0的时候创建新的
        mNativePtr = nativeCreate();
        mOwnsNativeParcelObject = true;
    }
}

nativeCreate是Native层的android_os_Parcel_create方法。其实现位于frameworks/base/core/jni/android_os_Parcel.cpp中,如下:

static jlong android_os_Parcel_create(JNIEnv* env, jclass clazz)
{
    Parcel* parcel = new Parcel();
    return reinterpret_cast<jlong>(parcel);
}

这里,将Native层生成的Parcel对象指针,通过reinterpret_cast<jlong>,转换为jlong类型,保存在Java变量mNativePtr中。即mNativePtr中保存的是Native层Parcel的指针。

reinterpret_cast<>用于处理类型无关的类型之间的强制转换。这里用来将指针parcel转换为long,其原理具体分析可以参考C++强制类型转换

下面我们来看看Parcel的构造实现,其源码位于frameworks/native/libs/binder/Parcel.cpp

Parcel::Parcel()
{
    initState();
}

void Parcel::initState()
{
    mError = NO_ERROR;  //错误码
    mData = 0;  //Parcel中存储的数据,它使一个uint8_t*类型的指针
    mDataSize = 0;  //Parcel中已存储的数据大小
    mDataCapacity = 0;  //最大存储能力
    mDataPos = 0;       //数据指针
    mObjects = NULL;
    mObjectsSize = 0;
    mObjectsCapacity = 0;
    mNextObjectHint = 0;
    mHasFds = false;
    mFdsKnown = true;
    mAllowFds = true;
    mOwner = NULL;
    mBlobAshmemSize = 0;
}

Parcel对象的构造过程只是简单的初始化了对象的属性。这里是因为Parcel设计的时候遵循了“动态扩展”原则,即使用的时候再申请内存避免浪费资源。

5 writeString16实现

writeString16最终都是调用以下方法来实现写数据。

status_t Parcel::writeString16(const char16_t* str, size_t len)
{
    if (str == NULL) return writeInt32(-1);//str为空的时候,写入-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); //将数据str复制到data所指位置。
            *reinterpret_cast<char16_t*>(data+len) = 0;//写入字符串结束符0
            return NO_ERROR;
        }
        err = mError;
    }
    return err;
}

以上通过writeInplace获取到数据写入位置,然后通过memcpy拷贝数据。以下分析获取数据写入位置的实现。

void* Parcel::writeInplace(size_t len)
{
    if (len > INT32_MAX) {  //超出范围或者负数直接返回
        return NULL;
    }
    const size_t padded = pad_size(len);//页对齐。

    if (mDataPos+padded < mDataPos) {   //padded为负数,返回
        return NULL;
    }

    if ((mDataPos+padded) <= mDataCapacity) {//没有超过最大容量
restart_write:
        uint8_t* const data = mData+mDataPos; //当前地址加偏移

        if (padded != len) {    //需要填充尾部的情况
#if BYTE_ORDER == BIG_ENDIAN    //大端
            static const uint32_t mask[4] = {
                0x00000000, 0xffffff00, 0xffff0000, 0xff000000
            };
#endif
#if BYTE_ORDER == LITTLE_ENDIAN //小端
            static const uint32_t mask[4] = {
                0x00000000, 0x00ffffff, 0x0000ffff, 0x000000ff
            };
#endif
                //使用mask中的1,填充尾部
            *reinterpret_cast<uint32_t*>(data+padded-4) &= mask[padded-len];
        }

        finishWrite(padded);//更新mDataPos,指向最新位置。
        return data;
    }

    status_t err = growData(padded);    //内存不够,新增空间
    if (err == NO_ERROR) goto restart_write;
    return NULL;
}

status_t Parcel::growData(size_t len)
{
    if (len > INT32_MAX) {
        return BAD_VALUE;
    }

    size_t newSize = ((mDataSize+len)*3)/2;
    return (newSize <= mDataSize)
            ? (status_t) NO_MEMORY
            : continueWrite(newSize);
}   

6 readString

读取部分最终调用的是readString16Inplace,

const char16_t* Parcel::readString16Inplace(size_t* outLen) const
{
    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;
}

这里调用了readInplace 来获取读取的位置,分析如下:

const void* Parcel::readInplace(size_t len) const
{
    if (len > INT32_MAX) {//长度不正常,直接返回NULL
        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);//更新mDataPos
        ALOGV("readInplace Setting data pos of %p to %zu", this, mDataPos);
        return data;
    }
    return NULL;
}

7 Parcel用法

obtain 从池中获取一个Parcel.

dataSize 得到当前Parcel对象的实际存储空间

dataCapacity 得到当前Parcel对象已分配的存储空间。

dataPostion 获取当前Parcel对象的偏移值,类似文件流指针的偏移值

setDataPostion 设置当前Parcel对象的偏移值。

recyle 清空,回收当前Parcel对象的内存

writeXxx 向当前Parcel对象写入数据,具有多重重载

readXxx 从当前Parcel对象读取数据,具有多重重载

示例程序

github

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值