千人千麵線上問題回放技術(下)
3.怎樣統一hook block
如果你只是想大概理解block的底層技術,你只需google一下即可。 如果你想全面深入的理解block底層技術,那網上的那些資料遠遠滿足不了你的需求。 只能閱讀蘋果編譯器clang源碼和列出比較有代表性的block例子源碼,然後轉成c語言和彙編,通過c語言結合彙編研究底層細節。
何謂oc block
- block就是閉包,跟回調函數callback很類似,閉包也是對象
- blcok的特點: 1.可有參數列表 2.可有返回值
3.有方法體 4.capture上下文變數 5.有對象引用計數的內存管理策略(block生命周期)
- block的一般存儲在內存中形態有三種 _NSConcretStackBlock(棧)_NSConcretGlobalBlock(全局)_NSConcretMallocBlock(堆)
系統底層怎樣表達block
我們先來看一下block的例子:
void test()
{
__block int var1 = 8; //上下文變數
NSString *var2 = @"我是第二個變數」; //上下文變數
void (^block)(int) = ^(int arg)//參數列表
{
var1 = 6;
NSLog(@"arg = %d,var1 = %d, var2 = %@", arg, var1, var2);
};
block(1);//調用block語法
dispatch_async(dispatch_get_global_queue(0, 0), ^
{
block(2); //非同步調用block
});
}
這段代碼首先定義兩個變數,接著定義一個block,最後調用block。
- 兩個變數:這兩個變數都是被block引用,第一個變數有關鍵字__block,表示可以在block里對該變數賦值,第二個變數沒有__block關鍵字,在block里只能讀,不能寫。
- 兩個調用block的語句:第一個直接在當前方法test()里調用,此時的block內存數據在棧上,第二個是非同步調用,就是說當執行block(2)時test()可能已經運行完了,test()調用棧可能已經被銷毀。那這種情況block的數據肯定不能在棧上,只能在堆上或者在全局區。
系統底層表達block比較重要的幾種數據結構如下:
- 注意:雖然底層是用這些結構體來表達block,但是它們並不是源碼,是二進位代碼
enum
{
BLOCK_REFCOUNT_MASK = (0xffff),
BLOCK_NEEDS_FREE = (1 << 24),
BLOCK_HAS_COPY_DISPOSE = (1 << 25),
BLOCK_HAS_CTOR = (1 << 26),//todo == BLOCK_HAS_CXX_OBJ?
BLOCK_IS_GC = (1 << 27),
BLOCK_IS_GLOBAL = (1 << 28),
BLOCK_HAS_DESCRIPTOR = (1 << 29),//todo == BLOCK_USE_STRET?
BLOCK_HAS_SIGNATURE = (1 << 30),
OBLOCK_HAS_EXTENDED_LAYOUT = (1 << 31)
};
enum
{
BLOCK_FIELD_IS_OBJECT = 3,
BLOCK_FIELD_IS_BLOCK = 7,
BLOCK_FIELD_IS_BYREF = 8,
OBLOCK_FIELD_IS_WEAK = 16,
OBLOCK_BYREF_CALLER = 128
};
typedef struct block_descriptor_head
{
unsigned long int reserved;
unsigned long int size; //表示主體block結構體的內存大小
}block_descriptor_head;
typedef struct block_descriptor_has_help
{
unsigned long int reserved;
unsigned long int size; //表示主體block結構體的內存大小
void (*copy)(void *dst, void *src);//當block被retain時會執行此函數指針
void (*dispose)(void *);//block被銷毀時調用
struct block_arg_var_descriptor *argVar;
}block_descriptor_has_help;
typedef struct block_descriptor_has_sig
{
unsigned long int reserved;
unsigned long int size;
const char *signature;//block的簽名信息
struct block_arg_var_descriptor *argVar;
}block_descriptor_has_sig;
typedef struct block_descriptor_has_all
{
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
const char *signature;
struct block_arg_var_descriptor *argVar;
}block_descriptor_has_all;
typedef struct block_info_1
{
void *isa;//表示當前blcok是在堆上還是在棧上,或在全局區_NSConcreteGlobalBlock
int flags; //對應上面的enum值,這些枚舉值是我從編譯器源碼拷貝過來的
int reserved;
void (*invoke)(void *, ...);//block對應的方法體(執行體,就是代碼段)
void *descriptor;//此處指向上面幾個結構體中的一個,具體哪一個根據flags值來定,它用來進一步來描述block信息
//從這個欄位開始起,後面的欄位表示的都是此block對外引用的變數。
NSString *var2;
byref_var1_1 var1;
} block_info_1;
這個例子中的block在底層表達大概如下圖:
首先用block_info_1來表達block本身,然後用block_desc_1來具體描述block相關信息(比如block_info_1結構體大小,在堆上還是在棧上?copy或dispose時調用哪個方法等等),然而block_desc_1具體是哪個結構體是由block_info_1中flags欄位來決定的,block_info_1里的invoke欄位是指向block方法體,即是代碼段。block的調用就是執行這個函數指針。由於var1是可寫的,所以需要設計一個結構體(byref_var1_1)來表達var1,為什麼var2直接用他原有的類型表達,而var1要用結構體來表達。篇幅有限,這個自己想想吧?
block小結
- 為了表達block,底層設計三種結構體:block_info_1,block_desc_1,byref_var1_1,三種函數指針: block invoke方法體,copy方法,dispose方法
- 其實表達block是非常複雜的,還涉及到block的生命周期,內存管理問題等等,我在這裡只是簡單的貫穿主流程來介紹的,很多細節都沒介紹。
怎樣統一hook block
通過上面的分析,得知oc里的block就是一個結構體指針,所以我在源碼里可以直接把它轉成結構體指針來處理。 統一hook block源碼如下
VoidfunBlock createNewBlock(VoidfunBlock orgblock, ReplayEventIns *blockEvent,bool isRecord)
{
if(orgblock && blockEvent)
{
VoidfunBlock newBlock = ^(void)
{
orgblock();
if(nil == blockEvent)
{
assert(0);
}
};
trace_block_layout *blockLayout = (__bridge trace_block_layout *)newBlock;
blockLayout->invoke = (void (*)(void *, ...))(isRecord?hook_var_block_callBack_record:hook_var_block_callBack_replay);
return newBlock;
}
return nil;
}
我們首先新建一個新的block newBlock,然後把原來的block orgblock 和 事件指令blockEvent包到新的blcok中,這樣達到引用的效果。然後把新的block轉成結構體指針,並把結構體指針中的欄位invoke(方法體)指向統一回調方法。你可能詫異新的block是沒有參數類型的,原來block是有參數類型,外面調用原來block傳遞參數時會不會引起crash?答案是否定的,因為這裡構造新的block時 我們只用block數據結構,block的回調方法欄位已經被閹割,回調方法已經指向統一方法了,這個統一方法可以接受任何類型的參數,包括沒有參數類型。這個統一方法也是彙編實現,代碼實現跟上面的彙編層代碼類似,這裡就不附上源碼了。
那怎樣在新的blcok里讀取原來的block和事件指令對象呢? 代碼如下:
void var_block_callback_start_record(trace_block_layout * blockLayout)
{
VoidfunBlock orgBlock = (__bridge VoidfunBlock)(*((void **)((char *)blockLayout + sizeof(trace_block_layout))));
ReplayEventIns *node = (__bridge ReplayEventIns *)(*((void **)((char *)blockLayout + 40)));
}
總結
- 本文大概介紹了問題回放框架,接著介紹三個關鍵技術。這三個技術相對比較深入,估計很多讀者理解起來比較費勁,請諒解!
- 如果對裡面的技術點感興趣,你可以關注我們的公眾號。我們後續會單獨對裡面的技術點詳細深入的分析發文。
- 如果覺得上面有錯誤的地方,請指出。謝謝```js
本文作者:閑魚技術
原文鏈接
更多技術乾貨敬請關注云棲社區知乎機構號:阿里云云棲社區 - 知乎
本文為雲棲社區原創內容,未經允許不得轉載。
推薦閱讀: