前言
当我们在调用远程方法时,需要在进程间传递参数以及返回结果。这种类似的处理方式,需要把数据与进程相关性去除,变成一种中间形式,然后按统一的接口进行读写操作。这样的机制,一般在高级编程语言里被称为序列化。
在android世界里处理数据的序列化操作的,使用了一种Parcel类,而能够处理数据序列化能力,则是通过Parcelable接口来实现。于是,当我们需要在进程间传输一个对象,则实现这一对象的类必须实现Parcelable接口里定义的相应属性或方法,而在使用这一对象时,则可以使用一个Parcel引用来处理传输时的基本操作。
Parcel和Serialize很类似,只是它是在内存中完成的序列化和反序列化,利用的是连续的内存空间,因此会更加高效。
序列化原因
序列化的原因基本可以归纳为以下三种情况:
- 永久性保存对象,保存对象的字节序列到本地文件中
- 通过序列化对象在网络中传递
- 通过序列化对象在进程间传递
序列化方法
android中实现序列化有两个选择:一是实现Serializable接口(是Java SE本身就支持的),一是实现Parcelable接口(是android持有功能,效率比实现Serializable接口高效,可用于Intent数据传递,也可以用于进程间通信IPC)。实现Serializable接口非常简单,声明一下就可以了,而实现Parcelable接口稍微复杂一些,但效率更高,推荐用这种方法提高性能。
选择序列化方法的原则
- 在使用内存的时候,Parcelable比Serializable性能高,所以推荐使用Parcelable
- Serializable使用了反射,过程比较慢,在序列化的时候会产生大量的临时变量,从而引起频繁的GC
- Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcelable不能很好的保证数据的持久性在外界有变化的情况下,尽管Serializable效率低点,但此时还是建议使用Serializable
接口实现与使用
Serializable的实现,只需要implements Serializable即可。这只是给对象打一个标记,系统会自动将其序列化。从源码中也可以看出Serializable是一个空实现接口。
12public interface Serializable {}Parcelable的实现,不仅需要implements Parcelable,还需要在类中添加一个静态成员变量CREATOR,这个变量需要实现Parcelable.Creator接口。接下来看一下Parcelable接口的源码。
1234567891011121314151617181920212223242526272829public interface Parcelable {public static final int PARCELABLE_WRITE_RETURN_VALUE = 0x0001;public static final int PARCELABLE_ELIDE_DUPLICATES = 0x0002;public static final int CONTENTS_FILE_DESCRIPTOR = 0x0001;// Parcelable所需要的接口方法之一,必须实现。这一方法作用很简单,就是通过返回的整形来描述这一Parcel 是起什么作用的,通过这一整形每个bit来描述其类型,一般会返回0public int describeContents();// Parcelable所需要的接口方法之二,必须实现。writeToParcel()方法的作用是发送,就是将类所需要传输的 属性写到Parcel里,被用来提供发送功能的Parcel,会作为第一个参数传入,于是在这个方法里都是使用 writeInt()、writeLong()写入到Parcel里。这一方法的第二个参数是一个flag值,可以用来指定这样的发送 是单向的还是双向的,可以与aidl的in、out、inout三种限定符匹配public void writeToParcel(Parcel dest, int flags);// CREATOR对象,Parcelable接口所需要的第三项,必须提供实现,但这是一个接口对象。CREATOR对象是使用模 版类Parcelable.Creator,套用到具体实现类得到的。这个CREATOR对象在很大程度上是一个工厂类,用于远程 对象在接收端的创建。从某种意义上来说,writeToParcel()与CREATOR是一一对应的,发送端进程通过 writeToParcel(),使用一个Parcel对象将中间结果保存起来,而接收端进程则会使用CREATOR对象把作为 Parcel对象的中间对象再恢复出来,通过类的初始化方法以这个Parcel对象为基础来创建对象public interface Creator<T> {// 这是Parcelable.Creator<T>模版类所必须实现的接口方法,提供从Parcel转义出新的对象的能力。接收 端来接收传输过来的Parcel对象时,便会以这一个接口方法来取得对象public T createFromParcel(Parcel source);// 这是Parcelable.Creator<T>模版类所必须实现的另一个接口方法,但这一方法用于创建多个这种实现了 Parcelable接口的类。通过这一方法,CREATOR对象不光能创建单个对象,也能返回多个创建好的空对 象,但多个对象不能以某个Parcel对象为基础创建,于是会使用默认的类初始化方法public T[] newArray(int size);}public interface ClassLoaderCreator<T> extends Creator<T> {public T createFromParcel(Parcel source, ClassLoader loader);}}
Parcel的使用与实现
Parcel是一个容器,可以包含数据或者对象引用,并且能够用于Binder的传输,同时支持序列化以及跨进程之后进行反序列化,同时其提供了很多方法帮助开发者完成这些功能。
Parcel的使用
在分析Parcel之前,首先按照分析流程,介绍下关于Parcel的相关常规使用。
首先是关于Parcel的获取。
|
|
接下里向Parcel这个容器中传入数据。
|
|
Parcel支持写入的数据还有很多。在完成数据的写入之后,就需要进行数据的序列化。
|
|
在经过上一步处理之后,返回一个byte数组,主要的IPC相关的操作主要就是围绕此byte数组进行的。同时,由于parcel的读写都是一个指针操作的,这一步涉及到native的操作,所以,在将数据写入之后,需要将指针手动指向最初的位置。
|
|
最后使用完Parcel还需要回收销毁。
|
|
在IPC的另一端,需要进行Parcel的获取处理。在进行了IPC操作后,一般读取出来的就是之前序列化的byte数组,所以,首先要进行一个反序列化操作。
|
|
此时得到的parcel就是一个正常的parcel对象,这时就可以将之前我们所存入的数据按照顺序进行获取。
|
|
读取完毕后,同样是需要对parcel进行一个回收操作。
|
|
Parcel的实现
Parcel像极了指针的操作,所以基本上可以确定Java层对于parcel的处理仅仅是一个封装代理,实际的实现在C/C++ native层,所以parcel的使用同样涉及到jni的使用。
接下来看一下Parcel的Java层实现。
首先需要进行一个Parcel的获取。
|
|
可以看到,Parcel的初始化,主要是使用一个对象池进行的,这样可以提高性能以及内存消耗。源码中定义的对象池有两个。
|
|
sOwnedPool主要是用来存储parcel的,obtain()方法首先会检索池子中的parcel对象,若是能取出parcel,那么将这个parcel返回,同时将这个位置置空,若是现在连池子都不存在的话,那么就直接新建一个parcel对象
接下来看一下如何去创建一个Parcel对象,也就是new这个过程,那么看看Parcel的构造方法。
|
|
|
|
这里首先对参数进行检查,因为初始化传入的参数是0,那么直接执行nativeCreate(),并且将标志位mOwnsNativeParcelObject置为true,表示这个parcel已经在native进行了创建。此处的nativeCreate()是一个本地方法,其具体实现要切换到native环境中,那么此时的分析要从jni进行了,在jni代码中,其实现为以下函数。
|
|
这是一个jni的实现,首先调用了native的初始化,并且,返回操作这个对象的指针。接下来继续看到C++层的实现。
|
|
|
|
可以看出,对parce的初始化,只是在native层初始化了一些数据值,在完成初始化之后,就将这个操作指针返回,这样就完成了parcel的初始化。初始化完毕之后,就可以进行数据的写入了,首先写入一个int型数据,其Java层实现如下。
|
|
可以看出,Java层就纯粹是一个对于native实现的封装了,这时候的分析来到jni。
|
|
这里注意两个参数,一个是mNativePtr,即之前传上去的指针,另一个是val,即需要写入的整型数据。再深入看一下写入的操作。
|
|
|
|
这个函数首先是一个断言检查,然后对输入的参数取size值,再加上之前已经移动的位置,判断是否超过了该Parcel所定义的能力值mDataCapacity。若是超过了能力值,那么直接将能力值进行扩大,扩大的值是val值的大小,并且,写入时候是以4字节对齐写入,通过PAD_SIZE(sizeof(T))宏定义来实现。
至此,Parcel就成功写入一个数据了。