Android Parcel 学习
在Android中,通常使用序列化时,谷歌官方都推荐我们使用Parcelable来实现,因为效率比jdk提供的Serializable要高很多(大约10倍)https://developer.android.com/reference/android/os/ParcelParcelContainer for a message (data and object references) that c
首先看下谷歌官方reference的描述:
https://developer.android.com/reference/android/os/Parcel
Parcel
Container for a message (data and object references) that can be sent through an IBinder. A Parcel can contain both flattened data that will be unflattened on the other side of the IPC (using the various methods here for writing specific types, or the general Parcelable
interface), and references to live IBinder
objects that will result in the other side receiving a proxy IBinder connected with the original IBinder in the Parcel.
Parcel is not a general-purpose serialization mechanism. This class (and the corresponding Parcelable
API for placing arbitrary objects into a Parcel) is designed as a high-performance IPC transport. As such, it is not appropriate to place any Parcel data in to persistent storage: changes in the underlying implementation of any of the data in the Parcel can render older data unreadable.
Parcelables
The Parcelable
protocol provides an extremely efficient (but low-level) protocol for objects to write and read themselves from Parcels. You can use the direct methods writeParcelable(android.os.Parcelable, int)
and readParcelable(java.lang.ClassLoader)
or writeParcelableArray(T[], int)
and readParcelableArray(java.lang.ClassLoader)
to write or read. These methods write both the class type and its data to the Parcel, allowing that class to be reconstructed from the appropriate class loader when later reading.
https://blog.csdn.net/qinjuning/article/details/6785517
https://blog.csdn.net/caowenbin/article/details/6532217
https://blog.csdn.net/caowenbin/article/details/6532238
https://juejin.cn/post/6844904050702434311
先从Serialize说起
JAVA中的Serialize机制,其作用是能将数据对象存入字节流当中,在需要时重新生成对象。主要应用是利用外部存储设备保存对象状态,以及通过网络传输对象等。
Android中的新的序列化机制
在Android系统中,定位为针对内存受限的设备,因此对性能要求更高,另外系统中采用了新的IPC(进程间通信)机制,必然要求使用性能更出色的对象传输方式。在这样的环境下,Parcel被设计出来,其定位就是轻量级的高效的对象序列化和反序列化机制。
基本的思路如下:
1. 整个读写全是在内存中进行,主要是通过malloc()、realloc()、memcpy()等内存操作进行,所以效率比JAVA序列化中使用外部存储器会高很多;
2. 读写时是4字节对齐的,可以看到#define PAD_SIZE(s) (((s)+3)&~3)这句宏定义就是在做这件事情;
3. 如果预分配的空间不够时newSize = ((mDataSize+len)*3)/2;会一次多分配50%;
4. 对于普通数据,使用的是mData内存地址,对于IBinder类型的数据以及FileDescriptor使用的是mObjects内存地址。后者是通过flatten_binder()和unflatten_binder()实现的,目的是反序列化时读出的对象就是原对象而不用重新new一个新对象。
简单点来说:Parcel就是一个存放读取数据的容器, android系统中的binder进程间通信(IPC)就使用了Parcel类来进行客户端与服务端数据的交互,而且AIDL的数据也是通过Parcel来交互的。在Java空间和C++都实现了Parcel,由于它在C/C++中,直接使用了内存来读取数据,因此,它更有效率。 他可以包含数据或者是对象引用,并且能够用于Binder的传输。同时支持序列化以及跨进程之后进行反序列化,同时其提供了很多方法帮助开发者完成这些功能。
看到了Parcel背后的机制,本质上把它当成一个Serialize就可以了,只是它是在内存中完成的序列化和反序列化,利用的是连续的内存空间,因此会更加高效。
就应用程序而言,最常见使用Parcel类的场景就是在Activity间传递数据。在Activity间使用Intent传递数据的时候,可以通过Parcelable机制传递复杂的对象。
与Serializable的区别
Parcelable
在对与只需要进行内存序列化的操作时很快,因为Serializable
需要频繁的进行I/O。Parcelable
实现较为复杂且要注意读写顺序的一致性,Serializable
相对来说实现很简单。Parcelable
不适合用于做数据持久化,而Serializable适合。
使用
import android.os.Parcel;
import android.os.Parcelable;
public class Book implements Parcelable {
private int id;
private String name;
//setter & getter & constructor
//...
//下面是实现Parcelable接口的内容
//除了要序列化特殊的文件描述符场景外,一般返回零就可以了
@Override
public int describeContents() {
return 0;
}
//序列化
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
dest.writeString(name);
}
public static final Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book createFromParcel(Parcel in) {
//自定义的私有构造函数,反序列化对应的成员变量值
return new Book(in);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
//根据反序列化得到的各个属性,生成与之前对象内容相同的对象
private Book(Parcel in) {
//切记反序列化的属性的顺序必须和之前写入的顺序一致!!
id = in.readInt();
name = in.readString();
}
}
在Activity
之间通过序列化的方式传递Book
对象的数据:
//传递
Book book = new Book(123, "Hello world");
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intent.putExtra("book_data", book);
startActivity(intent);
//接受
Book book = (Book) getIntent.getParcelableExtra("book_data);
分析
Parcelable
只是一个接口,实现是交给Parcel
对象进行的。比如我们在writeToParcel
时会调用Parcel
类中的方法,进入其中可以看到实现是交给native做的
...
public final void writeInt(int val) {
nativeWriteInt(mNativePtr, val);
}
...
@FastNative
private static native void nativeWriteInt(long nativePtr, int val);
@FastNative
private static native void nativeWriteLong(long nativePtr, long val);
@FastNative
private static native void nativeWriteFloat(long nativePtr, float val);
...
/frameworks/base/core/jni/android_os_Parcel.cpp
static void android_os_Parcel_writeInt(JNIEnv* env, jclass clazz, jlong nativePtr, jint val) {
//通过指针再解释强转成Parcel对象
Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
if (parcel != NULL) {
//最终还是调用的Parcel中的writeInt32函数
const status_t err = parcel->writeInt32(val);
if (err != NO_ERROR) {
signalExceptionForError(env, clazz, err);
}
}
}
...
//实际调用的是一个通用的模版方法
status_t Parcel::writeInt32(int32_t val)
{
return writeAligned(val);
}
...
//模版方法
//其中
//mData表示指向Parcel内存的首地址
//mDataPos表示指向Parcel空闲内存的首地址
//mDataCapacity表示Parcel分配内存的大小
template<class T>
status_t Parcel::writeAligned(T val) {
COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE(sizeof(T)) == sizeof(T));
//先判断加上val后会不会超过可用大小
if ((mDataPos+sizeof(val)) <= mDataCapacity) {
restart_write:
//reinterpret_cast是c++的再解释强制转换操作
//首先会计算mData + mDataPos得到物理地址,转成指向T类型的指针(T类型就是实际入参的类型)
//然后将val赋值给指针指向的内容
*reinterpret_cast<T*>(mData+mDataPos) = val;
//主要逻辑是修改mDataPos的偏移地址
//将偏移地址加上新增加的数据的字节数
return finishWrite(sizeof(val));
}
//如果超过了可用大小,执行增长函数
//之后再goto到上面的restart_write标签执行写入逻辑
status_t err = growData(sizeof(val));
if (err == NO_ERROR) goto restart_write;
return err;
}
通过上述代码分析,writeInt32
函数会将数据写入到一段共享的内存中,所以同理我们在readInt
时,也是通过Parcel
对象从该段内存中读取对应的值的
template<class T>
status_t Parcel::readAligned(T *pArg) const {
COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE(sizeof(T)) == sizeof(T));
if ((mDataPos+sizeof(T)) <= mDataSize) {
//获取读取数据的地址
const void* data = mData+mDataPos;
//mDataPos指向下一个数据
mDataPos += sizeof(T);
//根据数据指针类型取出数据
*pArg = *reinterpret_cast<const T*>(data);
return NO_ERROR;
} else {
return NOT_ENOUGH_DATA;
}
}
写数据时在一块Parcel内存地址中,写入12,34;读取数据时从起始地址(Android系统在读取时会将mDataPos重置为起始值)+指针类型对应的字节数来一一读取,首先读取12,然后读取34。
这也就是为什么我们在写序列化方法时,一定要将对应的成员变量的读取/写入顺序保持一致的原因。
更多推荐
所有评论(0)