android中的序列化机制原理

前言

当我们在调用远程方法时,需要在进程间传递参数以及返回结果。这种类似的处理方式,需要把数据与进程相关性去除,变成一种中间形式,然后按统一的接口进行读写操作。这样的机制,一般在高级编程语言里被称为序列化。

在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是一个空实现接口。

    1
    2
    public interface Serializable {
    }
  • Parcelable的实现,不仅需要implements Parcelable,还需要在类中添加一个静态成员变量CREATOR,这个变量需要实现Parcelable.Creator接口。接下来看一下Parcelable接口的源码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public 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来描述其类型,一般会返回0
    public 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的获取。

1
Parcel parcel = Parcel.ontain();

接下里向Parcel这个容器中传入数据。

1
2
parcel.writeInt(int val);
parcel.writeString(String str);

Parcel支持写入的数据还有很多。在完成数据的写入之后,就需要进行数据的序列化。

1
parcel.marshall();

在经过上一步处理之后,返回一个byte数组,主要的IPC相关的操作主要就是围绕此byte数组进行的。同时,由于parcel的读写都是一个指针操作的,这一步涉及到native的操作,所以,在将数据写入之后,需要将指针手动指向最初的位置。

1
parcel.setDataPosition(0);

最后使用完Parcel还需要回收销毁。

1
parcel.recycle();

在IPC的另一端,需要进行Parcel的获取处理。在进行了IPC操作后,一般读取出来的就是之前序列化的byte数组,所以,首先要进行一个反序列化操作。

1
parcel.unmarshall(byte[] data, int offset, int length);

此时得到的parcel就是一个正常的parcel对象,这时就可以将之前我们所存入的数据按照顺序进行获取。

1
2
parcel.readInt();
parcel.readString();

读取完毕后,同样是需要对parcel进行一个回收操作。

1
parcel.recycle();

Parcel的实现

Parcel像极了指针的操作,所以基本上可以确定Java层对于parcel的处理仅仅是一个封装代理,实际的实现在C/C++ native层,所以parcel的使用同样涉及到jni的使用。

接下来看一下Parcel的Java层实现。

首先需要进行一个Parcel的获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
}
}
return new Parcel(0);
}

可以看到,Parcel的初始化,主要是使用一个对象池进行的,这样可以提高性能以及内存消耗。源码中定义的对象池有两个。

1
2
3
private static final int POOL_SIZE = 6;
private static final Parcel[] sOwnedPool = new Parcel[POOL_SIZE];
private static final Parcel[] sHolderPool = new Parcel[POOL_SIZE];

sOwnedPool主要是用来存储parcel的,obtain()方法首先会检索池子中的parcel对象,若是能取出parcel,那么将这个parcel返回,同时将这个位置置空,若是现在连池子都不存在的话,那么就直接新建一个parcel对象

接下来看一下如何去创建一个Parcel对象,也就是new这个过程,那么看看Parcel的构造方法。

1
2
3
4
5
6
7
private Parcel(long nativePtr) {
if (DEBUG_RECYCLE) {
mStack = new RuntimeException();
}
//Log.i(TAG, "Initializing obj=0x" + Integer.toHexString(obj), mStack);
init(nativePtr);
}
1
2
3
4
5
6
7
8
9
private void init(long nativePtr) {
if (nativePtr != 0) {
mNativePtr = nativePtr;
mOwnsNativeParcelObject = false;
} else {
mNativePtr = nativeCreate();
mOwnsNativeParcelObject = true;
}
}

这里首先对参数进行检查,因为初始化传入的参数是0,那么直接执行nativeCreate(),并且将标志位mOwnsNativeParcelObject置为true,表示这个parcel已经在native进行了创建。此处的nativeCreate()是一个本地方法,其具体实现要切换到native环境中,那么此时的分析要从jni进行了,在jni代码中,其实现为以下函数。

1
2
3
4
5
static jint android_os_Parcel_create(JNIEnv* env, jclass clazz)
{
Parcel* parcel = new Parcel();
return reinterpret_cast<jint>(parcel);
}

这是一个jni的实现,首先调用了native的初始化,并且,返回操作这个对象的指针。接下来继续看到C++层的实现。

1
2
3
4
Parcel::Parcel()
{
initState();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Parcel::initState()
{
mError = NO_ERROR;
mData = 0;
mDataSize = 0;
mDataCapacity = 0;
mDataPos = 0;
ALOGV("initState Setting data size of %p to %d\n", this, mDataSize);
ALOGV("initState Setting data pos of %p to %d\n", this, mDataPos);
mObjects = NULL;
mObjectsSize = 0;
mObjectsCapacity = 0;
mNextObjectHint = 0;
mHasFds = false;
mFdsKnown = true;
mAllowFds = true;
mOwner = NULL;
}

可以看出,对parce的初始化,只是在native层初始化了一些数据值,在完成初始化之后,就将这个操作指针返回,这样就完成了parcel的初始化。初始化完毕之后,就可以进行数据的写入了,首先写入一个int型数据,其Java层实现如下。

1
2
3
public final void writeInt(int val) {
nativeWriteInt(mNativePtr, val);
}

可以看出,Java层就纯粹是一个对于native实现的封装了,这时候的分析来到jni。

1
2
3
4
5
6
7
8
static void android_os_Parcel_writeInt(JNIEnv* env, jclass clazz, jint nativePtr, jint val) {
// 指针实际上是一个整型地址值,所以这里使用强转将int值转化为parcel类型的指针,然后使用这个指针来操作 native的parcel对象
Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
const status_t err = parcel->writeInt32(val);
if (err != NO_ERROR) {
signalExceptionForError(env, clazz, err);
}
}

这里注意两个参数,一个是mNativePtr,即之前传上去的指针,另一个是val,即需要写入的整型数据。再深入看一下写入的操作。

1
2
3
4
status_t Parcel::writeInt32(int32_t val)
{
return writeAligned(val);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<class T>
status_t Parcel::writeAligned(T val) {
COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE(sizeof(T)) == sizeof(T));
if ((mDataPos+sizeof(val)) <= mDataCapacity) {
restart_write:
*reinterpret_cast<T*>(mData+mDataPos) = val;
return finishWrite(sizeof(val));
}
status_t err = growData(sizeof(val));
if (err == NO_ERROR) goto restart_write;
return err;
}

这个函数首先是一个断言检查,然后对输入的参数取size值,再加上之前已经移动的位置,判断是否超过了该Parcel所定义的能力值mDataCapacity。若是超过了能力值,那么直接将能力值进行扩大,扩大的值是val值的大小,并且,写入时候是以4字节对齐写入,通过PAD_SIZE(sizeof(T))宏定义来实现。

至此,Parcel就成功写入一个数据了。