Android Parcel爲何如此高效?

Android Parcel淺析

簡介

在這裏插入圖片描述
都說Parcel高效,android framework層大量使用Parcel,尤其是涉及Binder通信模塊,大量的跨進程(IPC)通信,使用到Parcel進行數據傳遞,而且官方建議Bundle使用更換爲Parcel,序列化方面也建議使用Parcelable替代,那爲什麼Parcel高效呢,今天我就試試從底層的角度來分析分析,如有不對的還望指正


爲什麼Bundle和Serializble差那麼點意思?

簡單說下下,Bundle內部採用的map鍵值對存儲,大家都知道map去get時都是要取hash計算,移動鏈表找到值,有一定的時間複雜度;
而Serializble呢,功能確實很強大,但是序列化時容易產生很多臨時變量,這對於GC的話又是一定的負擔;
那爲什麼Parcel就高效呢?
簡單來說,Parcel是在native層實現的,直接對內存操作,讀取數據時都是在一個大的int8_t*指針下進行的,根據這個指針以及偏移值讀取和寫入數據,從讀取效率來說是很高的,根據指針和偏移來讀取,而沒有經過算法去讀取,也不會對虛擬機造成GC負擔


Parcel原理剖析


Pacel複用池Pool

從java層入手,可以看到Parcel很多讀寫read/write操作都是native方法,裏面重點關注他的Parcel Pool複用池,也就是提前分配好很多Parcel對象,直接從Pool裏面拿,用完後歸還給Pool池,提高了分配效率,對於一些頻繁使用的場景,也解決了內存碎片和抖動的問題

    public static Parcel obtain() {
        final Parcel[] pool = sOwnedPool;
        synchronized (pool) {
            Parcel p;
            for (int i=0; i<POOL_SIZE; i++) {
            	//從池子中拿出Parcel,並對pool數組i置爲null
                p = pool[i];
                if (p != null) {
                    pool[i] = null;
                    if (DEBUG_RECYCLE) {
                        p.mStack = new RuntimeException();
                    }
                    p.mReadWriteHelper = ReadWriteHelper.DEFAULT;
                    return p;
                }
            }
        }
        return new Parcel(0);
    }
    public final void recycle() {
        if (DEBUG_RECYCLE) mStack = null;
        freeBuffer();

        final Parcel[] pool;
        
        if (mOwnsNativeParcelObject) {
            pool = sOwnedPool;
        } else {
            mNativePtr = 0;
            pool = sHolderPool;
        }

        synchronized (pool) {
            for (int i=0; i<POOL_SIZE; i++) {
            	//使用完後,歸還給pool
                if (pool[i] == null) {
                    pool[i] = this;
                    return;
                }
            }
        }
    }

android中很多場景都用到了複用池這一技術,如Handler裏面的Message

Parcel Native層剖析

基於android8.0源碼環境,Parcel位於:
/frameworks/native/libs/binder/include/binder/Parcel.h
/frameworks/native/libs/binder/Parcel.cpp

首先,Parcel類重點關注這幾個變量屬性:

uint8_t*            mData;		//數據都是裝到這個指針指向的內存
size_t              mDataSize;	//當前存儲的數據大小
size_t              mDataCapacity;  //mData目前總的容量大小(可能有部分空閒的)
mutable size_t      mDataPos;  //mData的有效數據長度,後續寫入數據都從這裏開始寫入
binder_size_t*      mObjects;   //記錄寫入對象Object偏移數組,寫入對象時有用
size_t              mObjectsSize;  //當前mObjects數組中有數據的長度,寫入對象時有用
size_t              mObjectsCapacity;  //mObjects的總長度,寫入對象時有用

我們大致瀏覽下Parcel的方法,如下圖:
write
追蹤這些方法,最終都會走到如下這個方法裏面去:
在這裏插入圖片描述
字節對齊寫入方法:
a. 宏定義COMPILE_TIME_ASSERT_FUNCTION_SCOPE主要是確定要字節對齊,是4或者4的整數倍
b. if條件這塊,判斷有效數據長度加上當前寫入值value有沒有超過總容量,沒有超過的話就直接給mData+mDataPos位置處寫入val;超過的話就要增加mData數據長度,然後重走restart_write代碼快
reinterpret_cast<T>(mData+mDataPos) = val;等式左邊是先偏移mDataPos指針,然後將該指針解釋爲T*指針,然後在解指針,將val值拷貝到mData中去,拷貝的方式按照val的數據類型長度寫入,而不是按照mData類型寫入

mData數據擴張,看growData函數

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;
    }    
	//擴展爲之前的1.5倍
    size_t newSize = ((mDataSize+len)*3)/2;
    return (newSize <= mDataSize)
            ? (status_t) NO_MEMORY
            : continueWrite(newSize);
}

按照邏輯走,會進入continueWrite這個函數中去,該函數代碼較長,只抽取關鍵部分代碼

status_t Parcel::continueWrite(size_t desired)
{
//省略部分邏輯
//重新分配內存,desired是擴容後的長度
 uint8_t* data = (uint8_t*)malloc(desired);
 if (!data) {
     mError = NO_MEMORY;
     return NO_MEMORY;
 }
if (mData) {
	//數據拷貝
     memcpy(data, mData, mDataSize < desired ? mDataSize : desired);
}
//重新賦值,更新長度size和容量capcity等
mData = data;
mObjects = objects;
mDataSize = (mDataSize < desired) ? mDataSize : desired;
ALOGV("continueWrite Setting data size of %p to %zu", this, mDataSize);
mDataCapacity = desired;
mObjectsSize = mObjectsCapacity = objectsSize;
mNextObjectHint = 0;
//省略部分邏輯
}

這裏走完後,就會返回到writeAligned函數裏面的restart_write裏面去,寫入val;另外,在read讀取時也是根據偏移mDataPos和mData確定讀取值的位置,在用reprinter_cast進行二進制拷貝出去,完成讀取;以上就是Parcel淺析過程,如有不對望指正

除以上基本的數據類型外,有時還會往Parcel裏面寫入Object或者struct數據結構體,當只寫入一個對象時還好,如果在一個Parcel對象裏面寫入多個對象時,在讀取時,我們如何確定每個讀取每個對象從哪裏開始讀,從哪裏開些讀取完成呢?This is a problem!

解決版本就是文章前面的幾個對象mObjects/mObjectsSize/mObjectsCapacity,其核心思路就是沒寫入一個Object時,記錄寫入前的mDataPos偏移,將他存放在mObjects[mObjectsSize]處,mObjectsSize在自增1,依次寫入的每個Object都是按照這麼處理,當讀取時倒序讀取,從mObjects[mObjectsSize]最後的一個開始讀,拿到偏移offset,從當前位置向後讀完mData即可,依次讀取

看看寫入對象的源碼就知道了:

status_t Parcel::writeObject(const flat_binder_object& val, bool nullMetaData)
{
	判斷mData加當前參數數據長度有否超過容量
    const bool enoughData = (mDataPos+sizeof(val)) <= mDataCapacity;
    判斷mObjectsSize是否小於總的Objects容量
    const bool enoughObjects = mObjectsSize < mObjectsCapacity;
    內存充足
    if (enoughData && enoughObjects) {
restart_write:
		二進制拷貝到mData+mDataPos偏移處
        *reinterpret_cast<flat_binder_object*>(mData+mDataPos) = val;

      	。。。。。。
	
        同事記錄偏移
        if (nullMetaData || val.binder != 0) {
        	mObjects數組記錄這個寫入對象的mDataPos偏移
            mObjects[mObjectsSize] = mDataPos;
            acquire_object(ProcessState::self(), val, this, &mOpenAshmemSize);
            同時size自增
            mObjectsSize++;
        }

        return finishWrite(sizeof(flat_binder_object));
    }
	如果mData也就是存放數據的數組長度不夠,就重新分配內存在拷貝過來
    if (!enoughData) {
        const status_t err = growData(sizeof(val));
        if (err != NO_ERROR) return err;
    }
    如果存放偏移的數組mObjects長度不夠也要重新爲他分配內存
    if (!enoughObjects) {
    	重新計算長度
        size_t newSize = ((mObjectsSize+2)*3)/2;
        if (newSize < mObjectsSize) return NO_MEMORY;   // overflow
        重新分配內存
        binder_size_t* objects = (binder_size_t*)realloc(mObjects, newSize*sizeof(binder_size_t));
        if (objects == NULL) return NO_MEMORY;
        重新設置mObject相關變量
        mObjects = objects;
        mObjectsCapacity = newSize;
    }

    goto restart_write;
}

以上代碼主要的任務就是,寫入數據到mData中,同時記錄寫入的偏移mDataPos到mObjects數組中去;讀取時,根據這個偏移去讀取即可


java層的Parcel和Native層的Parcel有什麼關係?

實質結論是,他們是一一對應的,Parcel.java有一個long型變量,保存了natvie層Parcel.cpp的指針,java層寫入/讀取數據都是通過這個指針,將數據寫入到Native層去的,看看java層的部分代碼:

保存了native層的指針
private long mNativePtr;
public final void writeInterfaceToken(String interfaceName) {
		寫入數據就通過這指針寫入到native層對象
       nativeWriteInterfaceToken(mNativePtr, interfaceName);
}


總結

從Parcel邏輯大致可以獲取兩個點:

  1. 對於頻繁使用某些實例,追求一定的效率(包括內存),可以採用對象複用池技術
  2. 對於一些需要高效的存儲和讀取數據場景,可以使用這種對內存直接操作,指針加偏移快速搞定
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章