序列化的使用場景
- 將對象數據保存到存儲設備中;
- 將對象數據用于網絡上傳輸;
- 將對象數據用于進程之間的傳輸受啥;
- 序列化對象的時候只是針對成員變量進行序列化,對靜態(tài)成員變量鸽心,方法無法進行序列化操作滚局;
Serializable 和 Parcelable
Android 開發(fā)的時候有兩種序列化對象的方式 Serializable
和 Parcelable
,開發(fā)的時候兩者之間還是有差異的;
Serializable
Serializable
是 Java 提供的一個序列化的接口再悼,為對象提供標準的序列化和反序列化操作核畴;通常情況下我們只需要將我們的目標類實現(xiàn) Serializable
接口即可,此外可以為當前需要序列化的類指定一個 serialVersionUID
用來輔助序列化和反序列化的過程:序列化的時候系統(tǒng)會把當前類的 serialVersionUID
寫入序列化的文件中(或其它中介), 當反序列化的時候系統(tǒng)回去檢測文件中的 serialVersionUID
是否與當前類的 serialVersionUID
一致冲九,如果一致谤草, 則證明當前序列化類的版本和當前類的版本一致可以實現(xiàn)序列化,不一致則說明當前類和序列化的類相比發(fā)生了某些變化(例如:成員變量的數量/類型發(fā)生了變化)是無法正常發(fā)序列化的莺奸;
通常我們在沒有指定 serialVersionUID
的情況下丑孩,每次序列化的時候系統(tǒng)會去自動計算當前類的 hash
值作為 serialVersionUID
,此時如果當前類的內容有所改動則 hash
就會改變灭贷,既無法正常的序列化温学;
我們可以手動的將當前類的 serialVersionUID
指定為 1L ,也可以用 IDE 幫我們生成對應的 hash
值作為 serialVersionUID
兩者的效果是一致的;
另外甚疟,靜態(tài)成員變量屬于類不屬于對象仗岖,所以不會參與序列化過程,其次如果使用 transient
關鍵字標記的成員變量不參與序列化的過程览妖;
Parcelable
Parcelable
接口是 Android 特有的接口轧拄,使用起來比 Serializable
相對復雜一點:
- 實現(xiàn)
Parcelable
接口; - 實現(xiàn)接口中的兩個方法
// 只有在當前對象中存在文件描述符時返回 1 其它都返回 0 即可
public int describeContents(){}
// dest:該對象用來將序列化的對象寫入到內存中
// flags 只有兩種值: 0 / 1 讽膏,標志為 1 時表示當前對象需要作為返回值返回檩电,不能立即釋放資源
// 基本上都是 0;
public void writeToParcel(Parcel dest, @WriteFlags int flags){}
- 實例化靜態(tài)內部對象
CREATOR
實現(xiàn)接口Parcelable.Creator
,實例化CREATOR
時要實現(xiàn)其中的兩個方法俐末,其中createFromParcel
的功能就是從Parcel中讀取我們存儲的對象料按。
兩者使用的區(qū)別
Serializable
和 Parcelable
都能實現(xiàn)序列化且都可以用于 Intent
間的數據傳遞,但是還是存在一定的區(qū)別:
-
Serializable
是 Java 中的序列化接口卓箫,使用起來開銷量相對較大(I/O的方式)载矿,序列化和反序列化的過程需要大量的 I/O 操作。 -
Parcelable
是 Android 中的序列化方式烹卒,用起來相對比較麻煩恢准,但是效率高(共享內存的方式),是 Android 推薦使用的 序列化方式甫题; -
Parcelable
主要用在內存序列化上,如果要將一個對象序列化到存儲設備中涂召,使用Serializable
會是更佳的選擇坠非;
Parcelable --> Parcel源碼解析
上面講到了 Parcelable
使用的效率會比 Serializable
更高,接下來我們就來分析下 Parcelable
的源碼來驗證這句話;
Parcelable
提供了 writeToParcel(Parcel dest, @WriteFlags int flags){}
方法并暴露了一個 Parcel
參數給開發(fā)者來操作需要緩存的數據果正,下面我們就來分析下 Parcel
是如何緩存數據的:
public final class Parcel {
// mNativePtr 非常的關鍵炎码,該值實際上是 Native 層的 Parcel 對象的指針地址
// 后續(xù)的數據讀取/寫入都是通過該指針地址來操作的
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 方法創(chuàng)建 native 層的 Native 對象秋泳,并返回其指針地址
mNativePtr = nativeCreate();
mOwnsNativeParcelObject = true;
}
}
// native 方法潦闲,創(chuàng)建 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
對象的 創(chuàng)建/讀/寫 操作實際上都是通過調用 native
方法來實現(xiàn)的,看到這里好像源碼已經跟不下去了卓起,因為下面的代碼就是 c/c++
的實現(xiàn)了和敬,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},
}
// 創(chuàng)建 native 的 Parcel 對象的方法,該方法在 Java 的 Parcel 對象創(chuàng)建 mNativePtr = 0 的時候調用
static jlong android_os_Parcel_create(JNIEnv* env, jclass clazz)
{
// 創(chuàng)建 native 層的 Parcel 對象
Parcel* parcel = new Parcel();
// 獲取 parcel 對象的指針地址返回給 Java 層
// 后續(xù)數據的讀/寫都是通過該指針地址來操作的
// 這里也就驗證了上面說的 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 首先會創(chuàng)建一個 native
方法的映射,然后在 nativeCreate()
方法中創(chuàng)建一個 C++
的 Parcel
對象然后將該對象的指針地址轉成 jlong
類型的數據返回給 java
層笆怠;而數據的讀取則是通過 nativeCreate()
方法中創(chuàng)建的 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(四個字節(jié)) 數據,mDataPos 的值就會增加 四個字節(jié)泡态,
// 下次寫入數據的時候搂漠,指針的位置就會移動到上次寫入的 int 數據的內存地址后面
*reinterpret_cast<T*>(mData+mDataPos) = val;
// 更新內存的偏移量,這里是 int 類型的數據某弦,因此 mDataPos 會增加 四個字節(jié)
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
{
// 根據上面寫入字符串的規(guī)則抗果,這里需要先讀取一個 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
創(chuàng)建一個對象實現(xiàn)Parcelable
接口奸晴,重寫writeToParcel()
方法冤馏,寫入調用對應的方法寫入需要緩存的數據; -
Java
層創(chuàng)建Parcel.class
對象(沒有傳入mNativePtr
)的時候會調用nativeCreate()
方法創(chuàng)建一個native
層的Parcel.cpp
對象并返回指針地址寄啼; -
Parcel.cpp
會開辟一塊連續(xù)的內存來緩存數據逮光,內存的首地址是mData
,內存的偏移量是mDataPos
; - 寫入一個數據的時候墩划,首先會將指針移動到對應的位置
mData + mDataPos
睦霎,再將數據寫入到內存中,然后重新計算mDataPos
的偏移量走诞,mDataPos += sizeof()
副女,下次再寫入數據的時候就會跟再上次寫入數據的后面;
- 如果寫入的數據是字符串蚣旱,由于字符串的長度是不定的碑幅,需要開辟的內存大小也是未知的,因此需要先寫入字符串的長度塞绿,然后根據字符串的長度計算需要開辟的內存大小沟涨,緩存字符串,因此字符串所需的最終內存大小應該是:
sizeof(int) + len * sizeof(char)
;
-
Parcel.cpp
開辟的是一塊連續(xù)的內存异吻,根據上面的讀寫規(guī)則裹赴,可以得出讀取數據的順序需要和寫入數據的順序一致喜庞; -
Parcel
序列化數據操作的是 內存 ,而Serializable
序列化數據操作的是 I/O 棋返,因此延都,Parcel
的性能會更優(yōu);