浅析Block的内部结构 , 及分析其是如何利用 NSInvocation 进行调用
Block通過Clang編譯器編譯成C++語言后,可以看到它其實是一個結構體。結構及成員變量的構成如下圖所示:
Block的結構中首地址指向的就是isa指針,因此Blcok其實也是我們OC中的對象。通過編譯器的處理成C++底層的代碼時,Block就是一個結構體,其代碼結構如下
struct __main_block_impl_0 {// impl結構體 struct __block_impl {void *isa; //block的isa指針int Flags; //位移枚舉標記(標記desc中有無 copy , dispose方法,有無方法簽名字符 Signature 等...)int Reserved;void *FuncPtr; //實現block的功能函數} impl ;struct __main_block_desc_0 {size_t reserved;size_t Block_size; //block 的 內存大小/** 以下兩個函數是在 isa 指針指向 _NSConcreteMallocBlock時才會有 **/void (*copy)(void); void (*dispose)(void);/** 以下字符串是在impl.flag 包含((1 << 30)這個值是才有的變量),對應oc中的方法簽名NSMethodSignature**/const char *signatureStr; } * Desc;/** 以下都是block捕獲的變量 ,變量順序和是否捕獲進來根據block的定義來決定 ,這里只是簡單舉例**/struct __Block_byref_var_0 *var ; // __block變量TestClass *__strong strongTestVar ; // strong 變量TestClass *__weak weakTestVar ; // weak 變量int a ; //局部普通數據類型int *b ;//局部靜態變量/**全局靜態變量是直接通過變量的地址訪問的不需要捕獲進來*/ } 復制代碼isa - Block 的類型(isa指針的指向)分為 3種
PS : Block 訪問全局變量或靜態變量 都是通過捕獲他們的地址進行內容訪問的,因為這些變量從定義的那一刻開始就確定了其地址,因此可以通過指針傳遞來捕獲到block內部進行訪問。而捕獲普通局部變量就不一樣,局部變量在函數返回后其內存有可能會被會回收掉,所以是不能通過捕獲局部變量的地址到block訪問的而是通過值傳遞來傳進block內部
Flags : 這是一個位移枚舉的變量,標記著block的一些屬性,比如
- 結構體的Desc中有無 copy個 dispose函數 (1 << 25)
- 結構體的Desc中有無 signatureStr type encodings (char * 類型字符串) (1<<30)
- ......
FuncPtr : 就是你定義block的內部邏輯實現函數的指針。通過編譯器把OC的代碼處理成c語言的函數后在block初始化時,用這個變量記錄函數的指針地址,當block被調用時就是執行這個函數指針指向的函數。
Desc. Block_size : block的內存占用空間的大小
Desc -> copy + dispose 函數 :block用于管理自身內存的函數
......
更詳細的 block底層源碼實現 以及 __block變量的原理 推薦閱讀 深入研究Block捕獲外部變量和__block實現原理 這篇文章
清楚了Block的內部結構后,我們來看下如何理由 NSInvocation進行調 用。
NSInvocation是一個OC中用來封裝消息發送的類,在Runtime的消息轉發的最后一個轉發步驟(Normal Forwarding)也有出現 NSInvocation 。 Normal Forwarding 首先調用 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector這個方法,向調用者返回一個selector 對應的方法簽名類 NSMethodSignature對象,如果沒有返回NSMethodSignature這個類對象的話 就會拋出找不到方法的錯誤,否則,就會利用返回的 NSMethodSignature對象 生成一個NSInvocation對象傳進- (void)forwardInvocation:(NSInvocation *)anInvocation 方法中完成消息轉發機制的最后一步。
首先根據上面分析的Block內部定義一個結構體 ,方便我們對block進行內部訪問。
struct BlockLayout {void *isa;int flags;int reserved;void (*invoke)(void *, ...);struct block_descriptor {unsigned long int reserved;unsigned long int size;void (*copy)(void *dst, void *src); // (1<<25)void (*dispose)(void *src);const char *signature; // (1<<30)} *descriptor;// 捕獲的變量 };enum {DescFlagsHasCopyDispose = (1 << 25),DescFlagsIsGlobal = (1 << 28),DescFlagsHasSignature = (1 << 30) }; typedef int BlockDescFlags;復制代碼然后定義一個簡單的block
void(^testBlock)(int a , int b) = ^(int a , int b){NSLog(@"成功調用了 block");NSLog(@"參數1 -> a = %d , 參數2 -> b = %d" , a , b); }; 復制代碼下面開始對Block內部進行訪問,獲取去signature(const char * )后生成NSInvoction并傳參調用。
//強轉為自定義的block結構體指針struct BlockLayout * blockLayoutPointer = (__bridge struct BlockLayout *)testBlock;int flags = blockLayoutPointer -> flags;if (flags & BlockDescFlagsHasSignature) { //有signature字符串void * signaturePoint = blockLayoutPointer -> descriptor;signaturePoint += sizeof(unsigned long int); //reservedsignaturePoint += sizeof(unsigned long int); //sizeif (flags & BlockDescFlagsHasCopyDispose) {signaturePoint += sizeof(void (*)(void *dst , void *src)); //copysignaturePoint += sizeof(void (*)(void *src)); //dispose}//拿到 signature 字符串內容const char * signatureStr = (* (const char **) signaturePoint);NSMethodSignature * blockSignature = [NSMethodSignature signatureWithObjCTypes:signatureStr];NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:blockSignature];invocation.target = testBlock;//將要傳緊block的參數int param1 = 10 ;int param2 = 20 ;// block(type encodeings 為 @?) 對應的 NSInvocation 第一個參數為 block本身// SEL(type encodeings 為 :) 對應的 NSInvocation 第一個參數為 selector 的 調用者(targat type encodeings 為 @) ,第二個參數這是 _cmd (方法本身類型為SEL)[invocation setArgument:¶m1 atIndex:1];[invocation setArgument:¶m2 atIndex:2];[invocation invoke];} 復制代碼看下打印 ,成功地利用NSInvocation對象調用了 Block。
2018-09-03 15:10:55.182181+0800 BlockWithNSInvocation[10694:872743] 成功調用了 block 2018-09-03 15:10:55.182430+0800 BlockWithNSInvocation[10694:872743] 參數1 -> a = 10 , 參數2 -> b = 20 Program ended with exit code: 0 復制代碼關于類型強轉
類型強轉其實并沒有改變目標變量的實際內存的數據,類型強轉其實就是告訴編譯器 目標變量 是我強轉的類型數據,你對這個變量訪問時按照我指定的變量類型來訪問即可。
總結
以上是生活随笔為你收集整理的浅析Block的内部结构 , 及分析其是如何利用 NSInvocation 进行调用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: go defer性能测试
- 下一篇: Kafka消费者APi