淺析Block的內部結構 , 及分析其是如何利用 NSInvocation 進行呼叫

lyuf發表於2018-09-18

Block通過Clang編譯器編譯成C++語言後,可以看到它其實是一個結構體。結構及成員變數的構成如下圖所示:

1194012-1739b7e85e46b4db.png

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種

  1. _NSConcreteStackBlock: 只用到外部區域性變數 , 且沒有強指標引用的block , 其實質上就是函式棧上的區域性變數,在當前函式呼叫完後:(恢復棧空間的時候),就會被釋放掉。
  2. _NSConcreteGlobalBlock: 完全沒有用到外部變數 ,或只用到全域性變數、靜態變數的block ,生命週期從建立到應用程式結束。

PS : Block 訪問全域性變數或靜態變數 都是通過捕獲他們的地址進行內容訪問的,因為這些變數從定義的那一刻開始就確定了其地址,因此可以通過指標傳遞來捕獲到block內部進行訪問。而捕獲普通區域性變數就不一樣,區域性變數在函式返回後其記憶體有可能會被會回收掉,所以是不能通過捕獲區域性變數的地址到block訪問的而是通過值傳遞來傳進block內部

  1. _NSConcreteMallocBlock (估計是我們最常解除的block型別了) :特點是有強指標引用,或者被帶有copy修飾的屬性引用,或者作為函式返回值返回時。

Flags : 這是一個位移列舉的變數,標記著block的一些屬性,比如

  • 結構體的Desc中有無 copydispose函式 (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)也有出現 NSInvocationNormal 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); //reserved
        signaturePoint += sizeof(unsigned long int); //size
        if (flags & BlockDescFlagsHasCopyDispose) {
             signaturePoint += sizeof(void (*)(void *dst , void *src)); //copy
             signaturePoint += 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:&param1 atIndex:1];
        [invocation setArgument:&param2 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
複製程式碼

關於型別強轉

型別強轉其實並沒有改變目標變數的實際記憶體的資料,型別強轉其實就是告訴編譯器 目標變數 是我強轉的型別資料,你對這個變數訪問時按照我指定的變數型別來訪問即可。

相關文章