多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
[TOC] ## Serializable(谨慎使用序列化) 序列化和持久化很相似,有些人甚至混为一谈,其实还是有区别的,**序列化是为了解决对象的传输问题,传输可以在线程之间、进程之间、内存外存之间、主机之间进行**。在安卓中绝大部分场景是通过Binder来进行对象的传输. ### serialVersionUID 如果没有指定UID,那么JDK会自动帮我们生成一个UID.手动指定这个UID可以减少运行时的开销,如果我们不加,系统后根据通过一套复杂的运算,自动赋值,该值与类名,接口名,成员名都有关. ### 增加了潜在的风险 引入了隐藏的构造函数 ### 测试的代价大 每个版本都得保证序列化和反序列化的成功 ### 保护字段不被序列化 1. transient关键字修饰的变量,不会被序列化;Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。 2. static关键字修饰的也不会被序列化, 因为static是类的特征,和对象无关。 3. 放在父类中,如果父类实现了序列化,子类默认实现序列化。 ### 原理 ![](https://img.kancloud.cn/6b/a4/6ba4a5d08cc26eec260570795eca819c_740x286.png) ## Parcelable ### 源码解析 首先我们在一个实体对象在实现parcelable的时候,这个时候,我们会重写writeToParcel方法,其中执行 dest.writeInt(this.offLineBtn); writeLong 等等类型的数据,实际是执行native方法,在这里我们就不分析各种数据类型的存取了,我们现在拿一个代表int来分析下,看下jni方法: ~~~ static void android_os_Parcel_writeInt(JNIEnv* env, jclass clazz, jint nativePtr, jint val) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); const status_t err = parcel->writeInt32(val); if (err != NO_ERROR) { signalExceptionForError(env, clazz, err); } } ~~~ 在这里我们要特别注意两个参数,一个是之前传上去的指针以及需要保存的int数据,这两个值分别是: (jint nativePtr,jint val) 首先是根据这个指针,这里说一下,指针实际上就是一个整型地址值,所以这里使用强转将int值转化为parcel类型的指针是可行的,然后使用这个指针来操作native的parcel对象,即: const status\_t err = parcel->writeInt32(val); writeInt32是调用了parcel中的方法,parcel的实现类是在Framework/native/libsbinderParcel.cpp,我们看下writeInt32方法: ~~~ status_t Parcel::writeInt32(int32_t val) { return writeAligned(val); } ~~~ ~~~ 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: *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; ~~~ 分析上面的之前,首先要知道mData、mDataPos、mDataCapacity三个变量的意义,mData指向parcel缓存的首地址,mDataCapacity表示parcel缓存容量(大小),mDataPos指向parcel缓存中空闲区域的首地址**,整个parcel缓存是一块连续的内存。** **物理地址 = 有效地址+偏移地址,首先会判断先写入的int数据的字节数是否超过了data的容量,如果没有超过,会执行数据的写入,**reinterpret\_cast是c++的一种再解释,强制转换,上面首先会将mData+mDataPos得到物理地址,转成指向T类型的指针(T类型就是你传进来的变量的类型),然后将val赋值给指针指向的内容。然后修改偏移地址,finishWrite(sizeof(val)): ~~~ status_t Parcel::finishWrite(size_t len) { if (len > INT32_MAX) { // don't accept size_t values which may have come from an // inadvertent conversion from a negative int. return BAD_VALUE; } //printf("Finish write of %d\n", len); mDataPos += len; ALOGV("finishWrite Setting data pos of %p to %zu", this, mDataPos); if (mDataPos > mDataSize) { mDataSize = mDataPos; ALOGV("finishWrite Setting data size of %p to %zu", this, mDataSize); } //printf("New pos=%d, size=%d\n", mDataPos, mDataSize); return NO_ERROR; } ~~~ 上面主要是将修改偏移地址,将偏移地址加上新增加的数据的字节数。 如果增加的数据大于容量的话,那么首先扩展parcel的缓存空间,growData(sizeof(val)): ~~~ status_t Parcel::growData(size_t len) { if (len > INT32_MAX) { // don't accept size_t values which may have come from an // inadvertent conversion from a negative int. return BAD_VALUE; } size_t newSize = ((mDataSize+len)*3)/2; return (newSize <= mDataSize) ? (status_t) NO_MEMORY : continueWrite(newSize); } ~~~ 扩展成功,就继续goto restart\_write,在writeAligned方法中有restart\_write,执行restart\_write后面code,写入数据。 通过上面的解释相信大家已经明白int类型的数据写入parcel缓存了,既然知道存数据,那我们也要明白取数据了,在取数据的时候,我们会通过this.age = in.readInt();来取得int类型数据 ~~~ static jint android_os_Parcel_readInt(jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { return parcel->readInt32(); } return 0; } ~~~ 调用的parcel的readInt32方法: ~~~ int32_t Parcel::readInt32() const { return readAligned<int32_t>(); } T Parcel::readAligned() const { T result; if (readAligned(&result) != NO_ERROR) { result = 0; } return result; } status_t Parcel::readAligned(T *pArg) const { COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T)); if ((mDataPos+sizeof(T)) <= mDataSize) { const void* data = mData+mDataPos; mDataPos += sizeof(T); *pArg = *reinterpret_cast<const T*>(data); return NO_ERROR; } else { return NOT_ENOUGH_DATA; } ~~~ 读取数据的时候,首先我们会从parcel的起始地址+parcel偏移地址,得到读取的数据的地址,然后取出数据,然后将parcel的偏移地址+取出的数据的字节数,这样指针就可以指向下一个数据,这样说太抽象了,举个例子: 比如我们现在有一个对象,里面是 ~~~ stu{ int age = 32; double score = 99; } ~~~ 我们在写数据的时候,会在一块parcel的内存地址中,写32,99,然后读取的时候,会从起始地址+读取的字节数,来一一读取,首先读取parcel起始地址指向的数据,取出32,然后将指针地址偏移int字节数,指针指向99的地址,然后读取99,然后取出数据,这也就是parcelable在实现的时候为什么需要存和读取的顺序需要一致的原因。 ### 参考资料 [Parcelable最强解析](https://segmentfault.com/a/1190000012522154#articleHeader1) ## Serializable 和Parcelable 的区别 ### 作用 Serializa ble的作用是为了保存对象的属性到本地文件、数据库、网络流、RMI(Remote Method Invocation)以方便数据传输,当然这种传输可以是程序内的也可以是两个程序间的。使用了反射技术,并且期间产生临时对象 Android的Parcelable的设计初衷是因为Serializable效率过慢,为了在程序内不同组件间以及不同Android程序间(AIDL)高效的传输数据而设计,这些数据仅在内存中存在,Parcelable是通过IBinder通信的消息的载体。 ### 效率及选择 Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如activity间传输数据,而Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable,因为android不同版本Parcelable可能不同,所以不推荐使用Parcelable进行数据持久化 ### 高级功能上 Serializable序列化不保存静态变量,可以使用transient关键字对部分字段不进行序列化,也可以覆盖writeObject、readObject方法以实现序列化过程自定义 如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话说,用transient关键字标记的成员变量不参与序列化过程。 ###编程实现 对于Serializable,类只需要实现Serializable接口,并提供一个序列化版本id(serialVersionUID)即可。 而Parcelable则需要实现writeToParcel、describeContents函数以及静态的CREATOR变量(AS有相关插件 一键生成所需方法),实际上就是将如何打包和解包的工作自己来定义,而序列化的这些操作完全由底层实现。 ## Bundle ### 源码解析 Bundle位于android.os包中,是一个final类,这就注定了Bundle不能被继承。Bundle继承自BaseBundle并实现了Cloneable和Parcelable两个接口,因此对Bundle源码的分析会结合着对BaseBundle源码进行分析。**由于实现了Cloneable和Parcelable接口**,因此以下几个重载是必不可少的: * public Object clone() * public int describeContents() * public void writeToParcel(Parcel parcel, int flags) * public void readFromParcel(Parcel parcel) * public static final Parcelable.Creator CREATOR = new Parcelable.Creator() 以上代码无需过多解释。 Bundle的功能是用来保存数据,那么必然提供了一系列Bundle的put与get方法族数据的方法,这些方法太多了,几乎能够存取任何类型的数据; ### 重点 Bundle之所以能以键值对的方式存储数据,实质上是因为它内部维护了一个ArrayMap,具体定义是在其父类BaseBundle中: ~~~ ArrayMap<String, ObjectmMap = null; ~~~ : ~~~ void putBoolean(String key, boolean value) { unparcel(); mMap.put(key, value); } ~~~ 这里的mMap就是ArrayMap了,存储数据就是把键值对保存到ArrayMap里。 布尔类型数据的读取源码如下: ~~~ boolean getBoolean(String key) { unparcel(); if (DEBUG) Log.d(TAG, "Getting boolean in "+ Integer.toHexString(System.identityHashCode(this))); return getBoolean(key, false); } ~~~ getBoolean(String key, boolean defaultValue)的具体实现如下: ~~~ boolean getBoolean(String key, boolean defaultValue) { unparcel(); Object o = mMap.get(key); if (o == null) { return defaultValue; } try { return (Boolean) o; } catch (ClassCastException e) { typeWarning(key, o, "Boolean", defaultValue, e); return defaultValue; } } ~~~ 数据读取的逻辑也很简单,就是通过key从ArrayMap里读出保存的数据,并转换成对应的类型返回,当没找到数据或发生类型转换异常时返回缺省值。 注意到这里出现了一个方法:unparcel(),它的具体源码如下: ~~~ synchronized void unparcel() { if (mParcelledData == null) { return; } if (mParcelledData == EMPTY_PARCEL) { if (mMap == null) { mMap = new ArrayMap<String, Object>(1); } else { mMap.erase(); } mParcelledData = null; return; } int N = mParcelledData.readInt();////通过parel可以看出这个方法通过调用native方法返回当前data的dataposition if (N < 0) { return; } if (mMap == null) { mMap = new ArrayMap<String, Object>(N); } else { mMap.erase(); mMap.ensureCapacity(N); } //读取数据并存放到mMap中 mParcelledData.readArrayMapInternal(mMap, N, mClassLoader); //回收解包完的数据 mParcelledData.recycle(); mParcelledData = null; } ~~~ ### 参考资料 [Bundle源码解析](https://blog.csdn.net/qq_33288248/article/details/70143275) [序列化原理](https://blog.csdn.net/u011315960/article/details/89963230)