程序员必懂小技巧之Parcelable

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: / 前言 /序列化,简单来说,就是将对象数据,按照一定的规则,转成一串有迹可循的二进制流,然后将此二进制流在双方中传输。其中,输出方按照约定的规则转译对象,接收方按照约定的规则进行翻译,从而获得有效数据。应对Android的日常开发中,出镜率最高的序列化手段无非Serializable、以及Parcelable。也常将二者进行比较,以其各自的优劣势来应对不同的场景。

/ 前言 /

序列化,简单来说,就是将对象数据,按照一定的规则,转成一串有迹可循的二进制流,然后将此二进制流在双方中传输。其中,输出方按照约定的规则转译对象,接收方按照约定的规则进行翻译,从而获得有效数据。


应对Android的日常开发中,出镜率最高的序列化手段无非Serializable、以及Parcelable。也常将二者进行比较,以其各自的优劣势来应对不同的场景。


Serializable有进行过分析(https://juejin.im/post/6850418112501268494),对于Serializable的整体实现也算有个认识,简单做个回顾:


Serializable 将对象当成一颗树,遍历并反射各个节点获取信息。期间产生很多中间变量来保存信息

提供来一些可实现的对象方法,可以在序列化、反序列化过程中做一些处理,比如替换对象、比如加解密,比如默认值(包括以对象为粒度的默认值)

可以实现 ObjectOutputStream.writeObjectOverride() 和 ObjectInputStream.readObjectOverride()来完全控制序列化、反序列化过程


本篇文章的目的,将分析Parcelable实现原理,一者可以明白其实现;二者可以更好地与Serializable进行比较;三者对于序列化所要到达的目的考量也会有较清晰的认识。


本文将会回答以下问题,如果你不知道答案,或许有些帮助:


Parcelable 如何实现

为什么序列化与反序列化要保持相同的顺序

能否自行实现Parcel

子类是否需要实现Parcelable

Parcelable 真的比 Serializable 快吗


/ Parcel存储 /

实际上,Parcelable的实现可以用一句话概括:按照顺序,将从标记处获取的信息,加以辅助信息逐个写入存储区域(看完后文会理解这段话)。因此对于Parcelable来说,存储就显得尤为重要。而对于存储,主要实现均由Parcel.cpp来完成。


Parcel.cpp的出现,是为了应对IPC过程中的数据传输问题而出现的,这一点从Parcel.cpp位于Binder包下可窥探一二。以及为了intercode communication,这一点从Java侧能享用,以及Parcel.cpp的存储方式能看出。


进程间通信需要序列化参与,而Serializable以Java实现,天然就无法解决此问题。得益于Parcel.cpp,Parcelable借势来处理一些对性能要求较高的场景了,比如面对Intent。

Parcel.cpp位于

platform/frameworks/native/libs/bilder/Parcel.cpp

对于理解Parcel.cpp,以下成员变量需要了解

uint8_t*            mData;      //缓存数据的首地址
    size_t              mDataSize;  //当前数据大小
    size_t              mDataCapacity; //数据总容量
    mutable size_t      mDataPos;   //下一个数据的首地址

用图理解是这样的

也就是说,Parcel.cpp实际上提供了一块连续的内存区域来存储数据。Parcel.cpp提供了很多的成员函数来写入数据,而大部分的写入操作,会调用到writeAligned()

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:
     // mData+mDataPos地址为指针T*,值为val
      *reinterpret_cast<T*>(mData+mDataPos) = val;
      // 修改偏移地址,也就是mDataPos
      return finishWrite(sizeof(val));
    }
    // 当前容量不够,增加容量
    status_t err = growData(sizeof(val));
    // 增加容量成功,跳转到restart_write执行
    if (err == NO_ERROR) goto restart_write;
    // NO_ERROR 代表函数按预期执行
    return err;
}

结合上图,Parcel.cpp存储的,实际上,是连续的各种类型的对象。也因此存在一条规则,即使用Parcel.cpp来存储,必须要清楚,在哪个位置,存储的是什么类型的数据,这是后话了。当然Parcel.cpp也提供了诸如write()等方法,来将一数据,通过memcpy()写入mDataPos后的存储区域。

有写入,就有读出,对应的函数为readAligned()

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) {
        //检查要读取的数据是否越界
        if (mObjectsSize > 0) {
            // 检查数据能否被正确读取
            status_t err = validateReadData(mDataPos + sizeof(T));
            if(err != NO_ERROR) {
                // 这段区域的数据无法正常读取,但是偏移值还是要修改                mDataPos += sizeof(T);
                return err;
            }
        }
        // 要读取的数据的物理地址    
        const void* data = mData+mDataPos;
        // 更新偏移地址
        mDataPos += sizeof(T);
        // data的数据类型为T,pArg指向它
        *pArg =  *reinterpret_cast<const T*>(data);
        return NO_ERROR;
    } else {
        return NOT_ENOUGH_DATA;
    }
}

Parcel.cpp对于存储区域的内容,没有做过多的限制,这也是足够灵活的原因。如果你愿意你大可以借助Parcel.cpp来实现自己的序列化方式,并能享受到Parcelable所能享受的优势。


在Java侧,当然不会让开发者直接操作Parcel.cpp,与之对应的也就是Parcel。为了便于区分,在之后的内容Parcel指的是Java侧的Parcel,Parcel.cpp如其名为C++的。

Parcel定义了足够多的Native方法,通过JNI,与Parcel.cpp建立连接并进行对应的操作,文件位于:

frameworks/base/core/jni/android_os_Parcel.cpp

Parcel在实例化时,通过Native方法nativeCreate()能拿到Parcel.cpp实例的句柄值,Parcel在做各种操作的时候,需要此句柄来操作Parcel.cpp。句柄存于Parcel成员变量mNativePtr。


一般来说,我们极少接触到Parcel,能利用到Parcel.cpp所带来的优势,均是通过Parcelable得以体验。Parcelable可以理解为系统实现的一套以Parcel为基础实现的序列化方案,除了必要之处,屏蔽了要对Parcel的了解。


实际上,对于Parcel的使用相当自由。如果你愿意以Parcel为基础实现一套序列化机制,解决序列化过程中的一些中间问题,如数据的读写规则、如何找到对应对象的数据等问题。也就可将Parcel用于你的其他场景。


/ Parcelable如何实现 /

使用Parcel要求实现Parcelable,其中:


实现writeToParcel(),指明序列化时要写出的数据

实现参数为Parcel的实例化方法,指明反序列化时,要读入的数据

实现static final成员变量Creator,以提供创建对象实例的入口


不妨从Activity的跳转过程看看Parcel的使用。


数据写入

首先,可以通过Bundle.putParcelable()向Intent传入额外参数。

    public void putParcelable(@Nullable String key, @Nullable Parcelable value) {
        // 解析Parcel,将Parcel中的数据,放入mMap
        unparcel();
        // 将Parcelable进行K,V存储
        mMap.put(key, value);
        mFlags &= ~FLAG_HAS_FDS_KNOWN;
    }

在Bundle中,mMap用来存储额外的数据。unparcel()是为了将数据统一放入mMap中,后续统一序列化。

在Activity启动过程中,由AMS完成进程通信,期间,将调用Intent.writeToParcel()将所有必要数据进行序列化,并完成传输。

public void writeToParcel(Parcel out, int flags) {
        .....  // 除了Bundle外,还会向out中写入其他各种信息,入mAction、mIdentifier、mPackage、mComponent等,此处忽略这些代码
        out.writeBundle(mExtras);
    }
-> Parcel.writeBundle()
-> Bundle.writeToParcel()
-> BaseBundle.writeToParcelInner()
    void writeToParcelInner(Parcel parcel, int flags) {
        if (parcel.hasReadWriteHelper()) {
            // 有ReadWriteHelper,默认为ReadWriteHelper.DEFAULT,
            // 将数据压入mMap
            unparcel();
        }
        final ArrayMap<String, Object> map;
        synchronized (this) {
            // 上面的unparcel()实际上,会将mParcelladData取出
            // 如果走到这里,说明数据经过辗转,又存到了mParcelladData
            // 也就是说这里和上面的if判断实际上应该是互斥的
            if (mParcelledData != null) {
                if (mParcelledData == NoImagePreloadHolder.EMPTY_PARCEL) {
                // 无数据,写入Int的0作为标志
                    parcel.writeInt(0);
                } else {
                    int length = mParcelledData.dataSize();
                    parcel.writeInt(length);
                    parcel.writeInt(mParcelledByNative ? BUNDLE_MAGIC_NATIVE : BUNDLE_MAGIC);
                    // 最终通过Parcel.cpp.appendFrom() 将mParcelledData的数据内容拷贝到parcel中
                    parcel.appendFrom(mParcelledData, 0, length);
                }
                return;
            }
            map = mMap;
        }
        // 无数据,写入Int的0作为标志
        if (map == null || map.size() <= 0) {
            parcel.writeInt(0);
            return;
        }
        // 实际上是 Parcel.cpp.mDataPos 
        int lengthPos = parcel.dataPosition();
        // 写入临时占用位
        parcel.writeInt(-1);
        // 写入魔数
        parcel.writeInt(BUNDLE_MAGIC);
        // 也是Parcel.cpp.mDataPos,但这个时候多了 占用位 + 魔数 长路的偏移量
        int startPos = parcel.dataPosition();
        // 写入map
        parcel.writeArrayMapInternal(map);
        // 最终Parcel.cpp.mDataPos的位置
        int endPos = parcel.dataPosition();
        // 回到lengthPos位置
        parcel.setDataPosition(lengthPos);
        int length = endPos - startPos;
        // 在前面占用的位置上,写入长度信息
        parcel.writeInt(length);
        // 恢复Parcel.cpp.mDataPos到正确的位置
        parcel.setDataPosition(endPos);
    }

以上需要注意的是:

1、mMap和mParcelladData是竞争关系,数据只会从其中一个位置存入

2、数据最终被写入Parcel.cpp中,其中,写入的数据,第一个数据为 魔数 + 真正数据的长度

 void writeArrayMapInternal(@Nullable ArrayMap<String, Object> val) {
        //
        final int N = val.size();
        // 写入有多少数据
        writeInt(N);
        int startPos;
        for (int i=0; i<N; i++) {
            if (DEBUG_ARRAY_MAP) startPos = dataPosition();
            // 写入 key
            writeString(val.keyAt(i));
            // 写入 Object
            writeValue(val.valueAt(i));
            //
        }
    }
public final void writeValue(@Nullable Object v) {
    //此方法将会按照v类型的不同,执行不同的写入,省略其他
    ......
    else if (v instanceof Parcelable) {
            // VAL_PARCELABLE 用来标记为Pacelable类型
            writeInt(VAL_PARCELABLE);
            writeParcelable((Parcelable) v, 0);
        }
    ......
}
    public final void writeParcelable(@Nullable Parcelable p, int parcelableFlags) {
        if (p == null) {
            writeString(null);
            return;
        }
        // 这里是写入类名
        writeParcelableCreator(p);
        // 调用对象类实现的writeToParcel()方法
        p.writeToParcel(this, parcelableFlags);
    }

序列化完后,Intent中的Parcel的存储情况是这样的:


左右部分为Activity启动过程写入的其他信息,length为Bundle数据长度,magic为标示,N为数据量,然后,是连续的K/V,K/V中包含一个Int,用来标志对象类型。


value的写入,就完全交给对象的实现类实现writeToParcel()来写入。value还可以继续往下细分格式,因为在Parcelable.writeToParcel()中写入数据时,最终还是会来到Parcel.writeValue()


数据读出

获取数据,则是通过Bundle.getParcelable()

   public <T extends Parcelable> T getParcelable(@Nullable String key) {
        unparcel();
        Object o = mMap.get(key);
        if (o == null) {
            return null;
        }
        try {
            return (T) o;
        } 
        .....
}
-> BaseBundle.unparcel()
-> BaseBundle.initializeFromParcelLocked()
-> Parcel.readArrayMapInternal()
    void readArrayMapInternal(@NonNull ArrayMap outVal, int N,
            @Nullable ClassLoader loader) {
            // N 是在前一步initializeFromParcelLocked()通过Parcel.readInt()读出
        ......
        // 数据起始位置
        int startPos;
        while (N > 0) {
            if (DEBUG_ARRAY_MAP) startPos = dataPosition();
            // key
            String key = readString();
            // 读出value,value包含int(标志对象类型)和数据信息
            Object value = readValue(loader);
            ......
            // 存入mMap中
            outVal.append(key, value);
            N--;
        }
        outVal.validate();
    }

在将数据从Parcel.cpp解压到Bundle.mMap中后,就可以使用K/V获取到具体的对象。在序列化时,有Parcel.writeValue()将对象按类型写入,在反序列化时,就有Parcel.readValue()按照对象类型读出,只看Parcelable相关部分

-> Parcel.readValue()
-> Parcel.readParcelable()
    public final <T extends Parcelable> T readParcelable(@Nullable ClassLoader loader) {
        // 获取到对象声明的public static final Creator<Phone> CREATOR
        Parcelable.Creator<?> creator = readParcelableCreator(loader);
        if (creator == null) {
            return null;
        }
        if (creator instanceof Parcelable.ClassLoaderCreator<?>) {
          Parcelable.ClassLoaderCreator<?> classLoaderCreator =
              (Parcelable.ClassLoaderCreator<?>) creator;
          // 执行createFromParcel()创建对象,并对象的数据填充
          return (T) classLoaderCreator.createFromParcel(this, loader);
        }
        return (T) creator.createFromParcel(this);
    }

readParcelableCreator()代码不贴出,内容是使用反射获取到对象类的以下声明

public static final Creator <xxx> CREATOR

段代码就解决了为什么要声明CREATOR,也解释了Parcelable如何反序列化。结合之前的内容,在序列化时,对象有执行时机,执行writeToParcel()来决定自身的数据如何写入;在反序列化时,通过CREATOR调用到带Parcel参数的实例化方来,来决定反序列化的出数据如何填充。


这也不难看出,为什么对象类在实现序列化与反序列化时,读入和写出的数据,要保持一致。但,这也不是绝对的。


之前说过,对于Parcel的使用可以非常灵活。对于读入和写出的数据要保持一致,也就意味着对象对内容有足够的控制权。借助系统Activity的启动过程看Parcel的内容变动,不难看出,需要解决的事写入的内容如何分布,而对有效用的内容不会做任何的拆解。


因此大可以在序列化时写入一些特殊的信息,在反序列化前取出即可。在使用Parcel进行序列化时,所有的信息存于一块连续的内存区域,包含助记信息、数据信息。反序列化就变成了如何借助助记信息,查找到数据信息了。如果跳出Activity的启动来看,想借助Parcel并实现一套序列化以应对其他的场景时,自然就要考虑信息如何分布,数据信息如何查找等问题了。


如果你想的话,你完全可以不实现Parcelable,使用Parcel提供的其他函数,也可以实现Parcel为核心的序列化,只不过比较麻烦罢了。


小结


Parcel序列化的原理总结为:


实际存储由Parcel.cpp负责,在一块连续的内存区域的中,写入各类信息。Parcel在实例化时,拿到Parcel.cpp的实例的句柄

Parcel在序列化写入数据时,遵循I-V的形式,其中I指数据类型,V指实际数据

要序列化的对象实现Parcelable,其中writeToParcel()在序列化过程中获得执行时机,对象负责写入数据。CREATOR在反序列化时被调用,调用对象构造函数,将Parcel传入以让对象填充数据

/ Parcelable VS Serializable /

Parcelable常会用来与Serializable进行比较,但Parcelable要比Serializable速度差异,需要进行测试对比。

准备以下序列化对象:

public class Phone implements Parcelable, Serializable {
    private String brand;
    private int price;
    public Phone(String brand, int price) {
        this.brand = brand;
        this.price = price;
    }
    protected Phone(Parcel in) {
        // 测试需要,这个是类名
        in.readString();
        brand = in.readString();
        price = in.readInt();
    }
    public static final Creator<Phone> CREATOR = new Creator<Phone>() {
        @Override
        public Phone createFromParcel(Parcel in) {
            return new Phone(in);
        }
        @Override
        public Phone[] newArray(int size) {
            return new Phone[size];
        }
    };
    @Override
    public int describeContents() {
        return 0;
    }
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(brand);
        dest.writeInt(price);
    }

接着是测试例子,计算两种方式的单次序列化和反序列化时间

 private void doParcalable() {
        long writeStart;
        long writeEnd;
        long readStart;
        long readEnd;
        Parcel parcel;
        int dataStartPos;
        Phone curPhone;
        parcel = Parcel.obtain();
        curPhone = createPhone();
        writeStart = System.nanoTime();
        dataStartPos = parcel.dataPosition();
        parcel.writeParcelable(curPhone, 0);
        writeEnd = System.nanoTime();
        int length = parcel.marshall().length;
        parcel.setDataPosition(dataStartPos);
        readStart = System.nanoTime();
        Phone.CREATOR.createFromParcel(parcel);
        readEnd = System.nanoTime();
        Log.d(TAG, "parcel: " +
                (writeEnd - writeStart) / 1_000 + "微秒; unparcel: " +
                (readEnd - readStart) / 1_000 +
                "微秒; Size: " + length);
    }
    private void doSerializable() {
        long writeStart;
        long writeEnd;
        long readStart;
        long readEnd;
        ByteArrayOutputStream dataOut;
        ByteArrayInputStream dataIn;
        try {
            ObjectOutputStream out;
            ObjectInputStream in;
            dataOut = new ByteArrayOutputStream();
            out = new ObjectOutputStream(dataOut);
            Phone phone = createPhone();
            writeStart = System.nanoTime();
            out.writeObject(phone);
            writeEnd = System.nanoTime();
            out.flush();
            byte[] data = dataOut.toByteArray();
            int lenght = data.length;
            dataIn = new ByteArrayInputStream(data);
            readStart = System.nanoTime();
            in = new ObjectInputStream(dataIn);
            in.readObject();
            readEnd = System.nanoTime();
            Log.d(TAG, "Serialiazable: " + (writeEnd - writeStart) / 1_000
                    + "微秒; unparcel: " + (readEnd - readStart) / 1_000
                    + " 微秒; Size: " + lenght);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

执行结果为


parcel: 67微秒; unparcel: 17微秒; Size: 68 Serialiazable: 4065微秒; unSerialiazable: 661 微秒; Size: 91


从执行结果上看Parcelable比Serializable快。其中,影响时间的因素有:


中间过程。根据Serializable的原理,序列化与反序列化过程要创建大量的中间变量来获取、存储数据。而Parcelable则不用,直接将各种需要的数据写入Parcel.cpp中

反射。Serializable使用了大量反射,而反射操作耗时。Parcelable使用了非常少的反射操作,来获取入口,而数据,由对象来读入写出,因此省略了Serializable中必要的通过反射才能获取数据的多数时间

存储方式。即数据的存储位置,以及数据本身和助记信息


原则上,比较是要让二者所面对的其他变量相同或者趋于相近才有益,但是因为其他成本,实验条件达不到。因为例子仅供参考,并不精确。

考虑到Serializable可以通过实现writeObject()和readObject()来定义自身对象的写入过程,对例子中添加以下内容

    //Phone
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        out.writeUTF(brand);
        out.writeInt(price);
    }
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        brand = in.readUTF();
        price = in.readInt();
    }

再执行程序,得到结果如下


parcel: 64微秒; unparcel: 18微秒; Size: 68 Serialiazable: 3264微秒; unSerialiazable: 577 微秒; Size: 93


Serialiazable确实更快了,以为Serialiazable原理来看,规划如何写入写出后,就减少部分了需要反射获取信息的过程,并且当属性越多时,效果越明显。但是还没有达到能与Parcelable媲美的程度。你认为就到这里就结束了吗


但其实还有当序列化一个超级大的对象图表(表示通过一个对象,拥有通过某路径能访问到其他很多的对象),并且每个对象有10个以上属性时,并且Serializable实现了writeObject()以及readObject(),在平均每台安卓设备上,Serializable序列化速度大于Parcelable 3.6倍,反序列化速度大于1.6倍.


例子是这样的,构建一种对象类型,此类型能能构成大对象图表

public class TreeNode implements Serializable, Parcelable {
    private static final long serialVersionUID = 1L;
    public List<TreeNode> children;
    public String string0;
    public String string1;
    public String string2;
    public int int0;
    public int int1;
    public int int2;
    public boolean boolean0;
    public boolean boolean1;
    public boolean boolean2;
    public TreeNode() {
    }
    ......
    // 这里就省略序列化代码
}

这个对象表示一个树结点。然后,构造一颗深度为5,除了叶节点外,每节点有10个字节点的树

   private TreeNode createNode(int level) {
        if (level < 4) {
            return createRootNode(level + 1);
        } else {
            return createSimpleNode();
        }
    }
    private TreeNode createRootNode(int level) {
        TreeNode root = createSimpleNode();
        root.children = new ArrayList<TreeNode>(10);
        for (int i = 0; i < 10; i++) {
            root.children.add(createNode(level));
        }
        return root;
    }
    private TreeNode createSimpleNode() {
        TreeNode root = new TreeNode();
        root.string0 = "aaaaaaaaaa";
        root.string1 = "bbbbbbbbbb";
        root.string2 = "cccccccccc";
        root.int0 = 111111111;
        root.int1 = 222222222;
        root.int2 = 333333333;
        root.boolean0 = true;
        root.boolean1 = false;
        root.boolean2 = true;
        return root;
    }

然后,运行程序序列化这个对象,结果如下:


serialize: 23ms; deserialize: 29ms; size: 614279 parcel: 134ms; unparcel: 40ms; size: 1871036


结果惊喜,序列化结果超过3.6倍,反序列化结果虽然没有达到1.6倍,但是也是Serializable更快。


为什么会这样呢?我在那个提问的回答中,没有找到确切的答案。我想尝试分析。


从上面的输出内容来看,parcel的方式比serialize的方式序列化后,生成了更多的byte信息,多出部分的信息,甚至比serialize方式的产生的所有byte信息多很多。byte的写入读出,必然影响时间,更多的数据要用更多的时间去处理,这一点不难理解。那么,多出的数据从何而来?


Serilazable原理

Serilazable的实现方式中,是有缓存的概念的,当一个对象被解析过后,将会缓存在HandleTable中,当下一次解析到同一种类型的对象后,便可以向二进制流中,写入对应的缓存索引即可。但是对于Parcel来说,没有这种概念,每一次的序列化都是独立的,每一个对象,都当作一种新的对象以及新的类型的方式来处理。


因此上面结果的差异,就在与助记信息的差异。除去了有效值必须占用的存储空间。


当一个TreeNode对象已经被序列化过,下一个TreeNode再被序列化时,会发生什么。


对Serilazable来说,需要 1字节的数据类型标示 + 2字节的对象类型缓存索引位置 + 1字节的新对象标记

对Parcel来说:每次进入无差异,为 2字节 * 类名长度 + 4字节类型标记 * 属性数量


虽然针对一种类型初次序列化时,Serilazable需要更多的助记信息,但再次序列化类型已被序列化过的对象时,Serilazable要的信息将少得多。


上面没有提到有效值的占用空间,如果有效值的字面量出现过的话,Serilazable需要的空间更说,只需要2字节的字面量缓存索引。因此,当序列化一个很大的对象图表时,并且大多数对象的类型相同时,Parcel需要更多的助记信息,也就产生了更多的byte数据要写入读出。


也因此,如果上面的对象图表例子中,对象的类型几乎不同时,Parcel将再次占上风。当然这只是我的猜想,我也没法写这个程序来证明。


针对于Parcel比Serilazable更快的例子的原因分析,有不对之处或有更好的见解,烦请指出。


上面的例子能说明一定的问题,Serilazable要受累于他所面对的I/O,毕竟,内存中的数据交换与其他设备中的I/O交换,性能上不是一个量级。而序列化过程,产生的信息量也将直观地影响效率。例子中,尽可能把二者面对的其他因素拉到相似的水平进行比较,如此更公平。

总结

小结处已经对Parcel做过小结,这里以二者序列化的比较作为结束。


Parcel:

有出入口方法,让对象自行写入读出数据

需要使用方自行补充助记信息

在连续的内存空间中存储信息

一般来说,在Java侧,以I-V方式存储信息

子类不一定需要实现


Serializable:

以反射方式获取全量信息,要解析出对象描述,属性描述

产生大量中间变量,当有描述缓存时,则可以复用

有入口方法,实现后能像Parcel一样,让对象决定读入写出的数据

存储方式依赖IO

子类需要实现


/ 回答 /


Parcelable 如何实现


有出入口方法,让对象自行写入读出数据。一般来说,需要实现writeToParcel()、带Parcel参数的实例化函数、以及声明类型为Parcelable.Creator 的静态成员变量CREATOR。数据的存储由Parcel.cpp 在内存中获取一片连续的内存区域来写入。通过Parcel,可向此内存区域写入需要的信息。对象存储的数据,一般以 I(标记的对象类型)-V(实际数据)方式来存储。在反序列化时,可通过CREATOR来访问对象初始化函数,来填充数据。


为什么序列化与反序列化要保持相同的顺序


对象的数据的写入读出由完全由对象自己决定,因此存在一个规则——“这个对象知道它面对的"Parcel里”有什么。保持顺序是需要的,但不是绝对的,因为对象知道它的数据在哪,所以他可以决定用不用或怎样用。


能否自行实现Parcel


能。完全可以以Parcel为基础,实现一套序列化,如果不用提供的出入口方法,实现起来叫麻烦而已。


子类是否需要实现Parcelable


看情况,因为对象决定自身数据的读入写出,当不需要子类的信息时,不需要调用到与子类有关的代码。


Parcelable 真的比 Serializable 快吗


看面对的场景。参考上面的例子,当要序列化的对象,所产生的信息前者远大于后者时,Parcelable自然更慢。


这里不妨反问一句:Serializable的开销真的更大吗?


想有更多的资料理解文章可点击我的github


相关文章
|
6月前
|
Java 测试技术 程序员
滚雪球学Java(21):Java正则表达式
【4月更文挑战第10天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
53 2
滚雪球学Java(21):Java正则表达式
|
5月前
|
Java 索引
那些年,我们追过的Java List——ArrayList与LinkedList的爱恨情仇
【6月更文挑战第17天】ArrayList与LinkedList,Java List接口的双子星,各有千秋。ArrayList基于数组,随机访问快速,但插入删除慢;LinkedList用链表实现,插入删除高效,但索引访问慢。两者在爱恨情仇中教会我们权衡选择,成为编程旅程中难忘的记忆。 ```
26 0
|
6月前
|
Java
PTA java程序设计(java语言基础答案)
PTA java程序设计(java语言基础答案)
|
存储 Java 测试技术
【java】前八章:快速回忆(一)
【java】前八章:快速回忆
105 0
|
Java
【java】前八章:快速回忆(三)
【java】前八章:快速回忆
90 0
|
存储 Java API
【java】前八章:快速回忆(二)
【java】前八章:快速回忆
86 0
|
Java 数据安全/隐私保护 Android开发
【JavaSE成神之路】聊聊封装这件事
哈喽,我是兔哥呀,今天就让我们继续这个JavaSE成神之路! 这一节啊,咱们要学习的内容是Java的封装。
110 0
|
Java
java学习第十四天笔记-面向对象251-this和super得使用总结
java学习第十四天笔记-面向对象251-this和super得使用总结
81 0
java学习第十四天笔记-面向对象251-this和super得使用总结
|
缓存 安全 算法
OMG!日常Java练习题(每天进步一点点系列
OMG!日常Java练习题(每天进步一点点系列
OMG!日常Java练习题(每天进步一点点系列
|
Oracle Java 关系型数据库
【Java从入门到头秃专栏 】(一)学在Java语法之前
【Java从入门到头秃专栏 】(一)学在Java语法之前
112 0
【Java从入门到头秃专栏 】(一)学在Java语法之前