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的方法,如下圖:
追蹤這些方法,最終都會走到如下這個方法裏面去:
字節對齊寫入方法:
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邏輯大致可以獲取兩個點:
- 對於頻繁使用某些實例,追求一定的效率(包括內存),可以採用對象複用池技術
- 對於一些需要高效的存儲和讀取數據場景,可以使用這種對內存直接操作,指針加偏移快速搞定