Parcel 共享内存分析
序列化的使用场景
- 将对象数据保存到存储设备中;
- 将对象数据用于网络上传输;
- 将对象数据用于进程之间的传输;
- 序列化对象的时候只是针对成员变量进行序列化,对静态成员变量,方法无法进行序列化操作;
Serializable 和 Parcelable
Android 开发的时候有两种序列化对象的方式 Serializable
和 Parcelable
,开发的时候两者之间还是有差异的;
Serializable
Serializable
是 Java 提供的一个序列化的接口,为对象提供标准的序列化和反序列化操作;通常情况下我们只需要将我们的目标类实现 Serializable
接口即可,此外可以为当前需要序列化的类指定一个 serialVersionUID
用来辅助序列化和反序列化的过程:序列化的时候系统会把当前类的 serialVersionUID
写入序列化的文件中(或其它中介), 当反序列化的时候系统回去检测文件中的 serialVersionUID
是否与当前类的 serialVersionUID
一致,如果一致, 则证明当前序列化类的版本和当前类的版本一致可以实现序列化,不一致则说明当前类和序列化的类相比发生了某些变化(例如:成员变量的数量/类型发生了变化)是无法正常发序列化的;
通常我们在没有指定 serialVersionUID
的情况下,每次序列化的时候系统会去自动计算当前类的 hash
值作为 serialVersionUID
,此时如果当前类的内容有所改动则 hash
就会改变,既无法正常的序列化;
我们可以手动的将当前类的 serialVersionUID
指定为 1L ,也可以用 IDE 帮我们生成对应的 hash
值作为 serialVersionUID
两者的效果是一致的;
另外,静态成员变量属于类不属于对象,所以不会参与序列化过程,其次如果使用 transient
关键字标记的成员变量不参与序列化的过程;
Parcelable
Parcelable
接口是 Android 特有的接口,使用起来比 Serializable
相对复杂一点:
- 实现
Parcelable
接口; - 实现接口中的两个方法
// 只有在当前对象中存在文件描述符时返回 1 其它都返回 0 即可
public int describeContents(){}
// dest:该对象用来将序列化的对象写入到内存中
// flags 只有两种值: 0 / 1 ,标志为 1 时表示当前对象需要作为返回值返回,不能立即释放资源
// 基本上都是 0;
public void writeToParcel(Parcel dest, @WriteFlags int flags){}
- 实例化静态内部对象
CREATOR
实现接口Parcelable.Creator
,实例化CREATOR
时要实现其中的两个方法,其中createFromParcel
的功能就是从Parcel中读取我们存储的对象。
两者使用的区别
Serializable
和 Parcelable
都能实现序列化且都可以用于 Intent
间的数据传递,但是还是存在一定的区别:
-
Serializable
是 Java 中的序列化接口,使用起来开销量相对较大(I/O的方式),序列化和反序列化的过程需要大量的 I/O 操作。 -
Parcelable
是 Android 中的序列化方式,用起来相对比较麻烦,但是效率高(共享内存的方式),是 Android 推荐使用的 序列化方式; -
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
对象来操作的,这里的 Parcel
为 C++
对象,对应的文件位置为: 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
序列化数据的过程,接下来我们用文字来归纳一下:
-
Java
创建一个对象实现Parcelable
接口,重写writeToParcel()
方法,写入调用对应的方法写入需要缓存的数据; -
Java
层创建Parcel.class
对象(没有传入mNativePtr
)的时候会调用nativeCreate()
方法创建一个native
层的Parcel.cpp
对象并返回指针地址; -
Parcel.cpp
会开辟一块连续的内存来缓存数据,内存的首地址是mData
,内存的偏移量是mDataPos
; - 写入一个数据的时候,首先会将指针移动到对应的位置
mData + mDataPos
,再将数据写入到内存中,然后重新计算mDataPos
的偏移量,mDataPos += sizeof()
,下次再写入数据的时候就会跟再上次写入数据的后面;
- 如果写入的数据是字符串,由于字符串的长度是不定的,需要开辟的内存大小也是未知的,因此需要先写入字符串的长度,然后根据字符串的长度计算需要开辟的内存大小,缓存字符串,因此字符串所需的最终内存大小应该是:
sizeof(int) + len * sizeof(char)
;
-
Parcel.cpp
开辟的是一块连续的内存,根据上面的读写规则,可以得出读取数据的顺序需要和写入数据的顺序一致; -
Parcel
序列化数据操作的是 内存 ,而Serializable
序列化数据操作的是 I/O ,因此,Parcel
的性能会更优;