Parcel 共享內存分析

序列化的使用場景

  1. 將對象數據保存到存儲設備中;
  2. 將對象數據用于網絡上傳輸;
  3. 將對象數據用于進程之間的傳輸受啥;
  4. 序列化對象的時候只是針對成員變量進行序列化,對靜態(tài)成員變量鸽心,方法無法進行序列化操作滚局;

Serializable 和 Parcelable

Android 開發(fā)的時候有兩種序列化對象的方式 SerializableParcelable ,開發(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 相對復雜一點:

  1. 實現(xiàn) Parcelable 接口;
  2. 實現(xiàn)接口中的兩個方法
// 只有在當前對象中存在文件描述符時返回 1 其它都返回 0 即可
public int describeContents(){}

// dest:該對象用來將序列化的對象寫入到內存中
// flags 只有兩種值: 0 / 1 讽膏,標志為 1 時表示當前對象需要作為返回值返回檩电,不能立即釋放資源
// 基本上都是 0;
public void writeToParcel(Parcel dest, @WriteFlags int flags){}
  1. 實例化靜態(tài)內部對象 CREATOR 實現(xiàn)接口 Parcelable.Creator,實例化 CREATOR 時要實現(xiàn)其中的兩個方法俐末,其中 createFromParcel 的功能就是從Parcel中讀取我們存儲的對象料按。
兩者使用的區(qū)別

SerializableParcelable 都能實現(xiàn)序列化且都可以用于 Intent 間的數據傳遞,但是還是存在一定的區(qū)別:

  1. Serializable 是 Java 中的序列化接口卓箫,使用起來開銷量相對較大(I/O的方式)载矿,序列化和反序列化的過程需要大量的 I/O 操作。
  2. Parcelable 是 Android 中的序列化方式烹卒,用起來相對比較麻煩恢准,但是效率高(共享內存的方式),是 Android 推薦使用的 序列化方式甫题;
  3. 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 對象來操作的铝耻,這里的 ParcelC++ 對象,對應的文件位置為: 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 序列化數據的過程,接下來我們用文字來歸納一下:

  1. Java 創(chuàng)建一個對象實現(xiàn) Parcelable 接口奸晴,重寫 writeToParcel() 方法冤馏,寫入調用對應的方法寫入需要緩存的數據;
  2. Java 層創(chuàng)建 Parcel.class 對象(沒有傳入mNativePtr )的時候會調用 nativeCreate() 方法創(chuàng)建一個 native 層的Parcel.cpp 對象并返回指針地址寄啼;
  3. Parcel.cpp 會開辟一塊連續(xù)的內存來緩存數據逮光,內存的首地址是mData ,內存的偏移量是 mDataPos
  4. 寫入一個數據的時候墩划,首先會將指針移動到對應的位置 mData + mDataPos睦霎,再將數據寫入到內存中,然后重新計算 mDataPos 的偏移量走诞,mDataPos += sizeof()副女,下次再寫入數據的時候就會跟再上次寫入數據的后面;
Parcel創(chuàng)建并寫入數據
  1. 如果寫入的數據是字符串蚣旱,由于字符串的長度是不定的碑幅,需要開辟的內存大小也是未知的,因此需要先寫入字符串的長度塞绿,然后根據字符串的長度計算需要開辟的內存大小沟涨,緩存字符串,因此字符串所需的最終內存大小應該是: sizeof(int) + len * sizeof(char);
Parcel寫入字符串
  1. Parcel.cpp 開辟的是一塊連續(xù)的內存异吻,根據上面的讀寫規(guī)則裹赴,可以得出讀取數據的順序需要和寫入數據的順序一致喜庞;
  2. Parcel 序列化數據操作的是 內存 ,而 Serializable 序列化數據操作的是 I/O 棋返,因此延都,Parcel 的性能會更優(yōu);
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末睛竣,一起剝皮案震驚了整個濱河市晰房,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌射沟,老刑警劉巖殊者,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異验夯,居然都是意外死亡猖吴,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門挥转,熙熙樓的掌柜王于貴愁眉苦臉地迎上來距误,“玉大人,你說我怎么就攤上這事扁位。” “怎么了趁俊?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵域仇,是天一觀的道長。 經常有香客問我寺擂,道長暇务,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任怔软,我火速辦了婚禮垦细,結果婚禮上,老公的妹妹穿的比我還像新娘挡逼。我一直安慰自己括改,他們只是感情好,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布家坎。 她就那樣靜靜地躺著嘱能,像睡著了一般。 火紅的嫁衣襯著肌膚如雪虱疏。 梳的紋絲不亂的頭發(fā)上惹骂,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機與錄音做瞪,去河邊找鬼对粪。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的著拭。 我是一名探鬼主播纱扭,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼茫死!你這毒婦竟也來了跪但?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤峦萎,失蹤者是張志新(化名)和其女友劉穎屡久,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體爱榔,經...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡被环,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了详幽。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片筛欢。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖唇聘,靈堂內的尸體忽然破棺而出版姑,到底是詐尸還是另有隱情,我是刑警寧澤迟郎,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布剥险,位于F島的核電站,受9級特大地震影響宪肖,放射性物質發(fā)生泄漏表制。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一控乾、第九天 我趴在偏房一處隱蔽的房頂上張望么介。 院中可真熱鬧,春花似錦蜕衡、人聲如沸壤短。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸽扁。三九已至,卻和暖如春镶骗,著一層夾襖步出監(jiān)牢的瞬間桶现,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工鼎姊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留骡和,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓慰于,卻偏偏與公主長得像钮科,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子婆赠,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354