Parcel-Binder流水線的打包俠

語言: CN / TW / HK

theme: cyanosis

前言

在我們日常開發中,有可能會接觸到Parcel,這是一個在android中非常有趣的類,本章將通過對Parcel的作用出發,瞭解到Parcel類設計的出發點,同時通過一個例子實踐,使我們能通過Parcel去監控我們的跨程序資料傳輸的資料量。

Parcel 作用

我們在業務開發很有可能會遇到跨程序通訊相關的場景,比如我們常用的跨程序是通過Binder機制去實現的,當然,本章跟Binder沒有什麼關係啦!我們來想一下,如果我們傳輸相關的“資料”給另一個程序,我們怎麼做呢?這裡有兩個方面,如果是基本型別,比如int,我們想要在程序1中傳遞到程序2中,其實不斷把資料複製過去就可以了,但如果是一個物件(Object)呢?如果只是把某個物件傳遞,當然,這個物件本質只是一個記憶體地址對吧!比如我有一個Object,假設引用是Ox8000,物件的值0x16555,這個記憶體值在程序1中是有意義的,但是到了程序2,同樣的引用值有意義嗎?答案肯定是否定的,因為兩個程序都有自己獨立的記憶體地址,因此單純傳遞一個物件是沒有意義的。

通過上面的闡述,我們能夠知道在程序間中進行資料傳遞,需要解決這樣一類問題。那麼如果我們只把程序1的資料進行一個“打包”,傳遞到程序2中,我們只需要在程序2中還原一下資料的內容(區別於上述的地址),是不是就能實現資料的傳遞了呢!沒錯,Parcel就是為了解決這個問題而誕生的。

理解Parcel

我們常用的Parcel,可以這樣用

val parcel = Parcel.obtain() parcel.writeString(“xxx”) parcel.writeParcelable(Parcelable) parcel.recycle()

初始化部分

那麼從這個例子出發,我們看看Parcel內部做了什麼趣事。首先採用了Parcel.obtain獲取一個Parcel,我們在外部是不能直接獲取的,聰明的小夥伴肯定就知道了,其實這就是一個物件池的封裝

``` public static Parcel obtain() { Parcel res = null; synchronized (sPoolSync) { 物件池 if (sOwnedPool != null) { 嘗試從sOwnedPool獲取Parcel物件 res = sOwnedPool; sOwnedPool因為已經被使用了,此時就指向了下一個未使用的Parcel sOwnedPool = res.mPoolNext; res.mPoolNext = null; sOwnedPoolSize--; } }

如果物件池沒有快取,就新建一個
if (res == null) {
    res = new Parcel(0);
} else {
    if (DEBUG_RECYCLE) {
        res.mStack = new RuntimeException();
    }
    res.mReadWriteHelper = ReadWriteHelper.DEFAULT;
}
return res;

} ```

我們繼續看一個,Parcel初始化幹了什麼 ``` private Parcel(long nativePtr) { if (DEBUG_RECYCLE) { mStack = new RuntimeException(); } //Log.i(TAG, "Initializing obj=0x" + Integer.toHexString(obj), mStack); init(nativePtr); }

private void init(long nativePtr) { if (nativePtr != 0) { mNativePtr = nativePtr; mOwnsNativeParcelObject = false; } else { 一開始先執行這裡 mNativePtr = nativeCreate(); mOwnsNativeParcelObject = true; } } ``` 回顧一下上面的obtain,一開始物件池裡面Parcel都沒有,肯定會走到new Parcel(0)裡面,此時Parcel建構函式傳入的就是0,這裡呼叫了nativeCreate方法,它是一個jni呼叫,返回值儲存在mNativePtr變數中,那麼我們其實就可以猜測了,Java層的Parcel,其實本質還是一個殼,真正進行資料傳遞儲存的地方,肯定還是在native層。

我們可以通過Parcel的native實現,找到對應的jni註冊關係 {"nativeCreate", "()J", (void*)android_os_Parcel_create}, nativeCreate其實最終就會呼叫到android_os_Parcel_create static jlong android_os_Parcel_create(JNIEnv* env, jclass clazz) { Parcel* parcel = new Parcel(); return reinterpret_cast<jlong>(parcel); } 我們可以看到,這裡在native中new了一個Parcel(Parcel.cpp),這個才是真正的Parcel,緊接著把parcel這個指標返回了。所以這裡我們就知道了,在java層中的mNativePtr,其實就儲存著native層中Parcel的指標,這裡跟Thread類的實現有異曲同工之妙。

那麼我們繼續看空,這個native層的Parcel幹了什麼,我們直接看它的建構函式 Parcel::Parcel() { LOG_ALLOC("Parcel %p: constructing", this); initState(); } ```

void Parcel::initState() { LOG_ALLOC("Parcel %p: initState", this); mError = NO_ERROR; 錯誤嗎 mData = nullptr; Parcel中儲存的資料,它是一個指標 mDataSize = 0; Parcel已經儲存的資料 mDataCapacity = 0; 最大儲存空間 mDataPos = 0; 資料指標 ALOGV("initState Setting data size of %p to %zu", this, mDataSize); ALOGV("initState Setting data pos of %p to %zu", this, mDataPos); mVariantFields.emplace(); mAllowFds = true; mDeallocZero = false; mOwner = nullptr; mEnforceNoDataAvail = true; } ``` 這裡很有趣,只是初始化了幾個成員變數,賦予初始值,這裡需要注意的是,這裡僅僅只是初始化,ing沒有進行真正的記憶體分配,這裡也是動態擴充套件的原則,只有這個Parcel真正被使用的時候,才進行記憶體的分配。同時我們也看到了幾個關鍵的變數,mData,mDataSize,mDataCapacity,mDataPos,他們的關係就是:

image.png

圖片來自Parcelable 是如何實現的

使用部分

我們已經從上面的初始化部分,瞭解到了一個Parcel是怎麼被創建出來的,接著我們再看一下其使用,我們以writeString為出發點,解析一下其內部的原理,我們呼叫writeString,最終會被呼叫到一個jni函式,nativeWriteString16,它的真正實現在 {"nativeWriteString16", "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeString16}, 可以看到,我們在java層的一切writeXXX操作,都會被切換到native中執行

我們以android11的分支為例子,不同版本有一些實現的差異 static void android_os_Parcel_writeString16(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) { 最後還是通過Parcel類的方法writeString16進行實現,外面都是檢查 err = parcel->writeString16( reinterpret_cast<const char16_t*>(str), env->GetStringLength(val)); env->ReleaseStringCritical(val, str); } } else { err = parcel->writeString16(NULL, 0); } if (err != NO_ERROR) { signalExceptionForError(env, clazz, err); } } } 我們可以知道,android_os_Parcel_writeString16其實還是一個殼,用於一些校驗檢查,真正實現在writeString16這個方法中 ``` Parcel.cpp

status_t Parcel::writeString16(const char16_t* str, size_t len) { if (str == nullptr) return writeInt32(-1);

// NOTE: Keep this logic in sync with android_os_Parcel.cpp
 先寫入了當前資料的長度
status_t err = writeInt32(len);
if (err == NO_ERROR) {
len *= sizeof(char16_t);
writeInplace計算複製資料的目標所在的地址,data是怎麼找到的,需要注意這個函式
uint8_t* data = (uint8_t*)writeInplace(len+sizeof(char16_t));
if (data) {
通過writeInplace拿到了資料,最後通過memcpy把資料拷貝到目標程序的記憶體空間
memcpy(data, str, len);
*reinterpret_cast<char16_t*>(data+len) = 0;
return NO_ERROR;
}
err = mError;
}
return err;
}

``` 還記得我們一直說,Parcel其實要做到把程序1的記憶體資料打包,然後在程序2中還原,還原的過程就是writeInplace,最後通過memcpy把資料拷貝過去

``` void* Parcel::writeInplace(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 nullptr; } 進行了資料對齊,比如我們以長度為4對齊是,此時len為3,也需要填充為4 const size_t padded = pad_size(len);

   檢查是否溢位,我們記得上面那個圖,mDataPos就是當前資料的指標,如果加上padded後,產生溢位,就會使得mDataPos+padded < mDataPos
    if (mDataPos+padded < mDataPos) {
    return nullptr;
    }
    當前資料是否超過了最大容量mDataCapacity
    if ((mDataPos+padded) <= mDataCapacity) {
    restart_write:
    //printf("Writing %ld bytes, padded to %ld\n", len, padded);
    uint8_t* const data = mData+mDataPos;

    判斷採用BIG_ENDIAN還是LITTLE_ENDIAN方式填充
    if (padded != len) {
    #if BYTE_ORDER == BIG_ENDIAN

static const uint32_t mask[4] = { 0x00000000, 0xffffff00, 0xffff0000, 0xff000000 }; #endif #if BYTE_ORDER == LITTLE_ENDIAN static const uint32_t mask[4] = { 0x00000000, 0x00ffffff, 0x0000ffff, 0x000000ff }; #endif //printf("Applying pad mask: %p to %p\n", (void)mask[padded-len], // reinterpret_cast(data+padded-4)); reinterpret_cast(data+padded-4) &= mask[padded-len]; } 更新資料指標mDataPos finishWrite(padded); return data; } 如果執行到這裡,說明上面的操作已經超過了Parcel的儲存空間大小,需要呼叫growData進行擴容 status_t err = growData(padded); 擴容完成後,呼叫restart_write重新來一次分配過程 if (err == NO_ERROR) goto restart_write; return nullptr; } ``` 擴容的手段也是很簡單,新size = ((mDataSize+len)3)/2,即擴容了儲存資料後的1.5倍,期間也會判斷是否超過SIZE_MAX 這個巨集定義

``` 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; }

if (len > SIZE_MAX - mDataSize) return NO_MEMORY; // overflow
if (mDataSize + len > SIZE_MAX / 3) return NO_MEMORY; // overflow
size_t newSize = ((mDataSize+len)*3)/2;
return (newSize <= mDataSize)
? (status_t) NO_MEMORY
: continueWrite(std::max(newSize, (size_t) 128));
}

```

擴充套件實踐

我們經過了一大串的原始碼解析,相信我們能夠理解Parcel這個類的是怎麼實現的了,那麼瞭解這個有什麼用呢?嗯!我們從實踐出發才能真正獲取到知識。

實戰:在專案中,大家可能會遇到TransactionTooLargeException,這是因為進行binder傳輸的時候,資料量過大導致的?可能大家會問,我們專案中哪裡用到binder了?其實我們最熟悉的startActivity就用到了binder進行跨程序傳輸,只是細節被android封裝起來罷了。還有比如onSaveInstance,儲存資料的時候,其實也是。如果一個Bundle資料過大,或者傳輸的Parcelable資料過大,就會觸發TransactionTooLargeException,然後在實際專案中,我們怎麼知道一個Bundle或者Parcelable資料的實際大小呢?這裡需要注意一些,這裡的實際大小並不是單單指這個資料的大小,而是跨程序通訊時打包後的大小,那我們打包後的大小怎麼算呢?這個時候Parcel就登場了

image.png 我們Binder通訊其實就是用的Parcel進行資料打包的,所以判斷一個Bundle的大小,就可以用以下方式

private fun sizeAsParcel(bundle: Bundle): Int { val parcel = Parcel.obtain() try { parcel.writeBundle(bundle) return parcel.dataSize() } finally { parcel.recycle() } } 當然,Parcelable的資料也可以知道(比如我們的Intent就是實現了Parcelable) fun sizeAsParcel(parcelable: Parcelable): Int { val parcel = Parcel.obtain() try { parcel.writeParcelable(parcelable, 0) return parcel.dataSize() } finally { parcel.recycle() } } 知道大小後,我們在startActivitiy或者onSaveInstance這些需要進行binder通訊的地方,通過插樁或者監聽系統回撥的方式,就能做到一個卡口了!這裡不是本篇的重點,所以沒列出具體實現,在之後我們通過這種方式,實現一個parcelable的資料大小監控

總結

最後,感謝觀看!!

連結

https://developer.android.google.cn/reference/android/os/Parcel?hl=en