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

Block經過Clang編譯器編譯成C++語言後,能夠看到它實際上是一個結構體。結構及成員變量的構成以下圖所示:bash

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種ui

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

PS : Block 訪問全局變量或靜態變量 都是經過捕獲他們的地址進行內容訪問的,由於這些變量從定義的那一刻開始就肯定了其地址,所以能夠經過指針傳遞來捕獲到block內部進行訪問。而捕獲普通局部變量就不同,局部變量在函數返回後其內存有可能會被會回收掉,因此是不能經過捕獲局部變量的地址到block訪問的而是經過值傳遞來傳進block內部spa

  1. _NSConcreteMallocBlock (估計是咱們最常解除的block類型了) :特色是有強指針引用,或者被帶有copy修飾的屬性引用,或者做爲函數返回值返回時。

Flags : 這是一個位移枚舉的變量,標記着block的一些屬性,好比3d

  • 結構體的Desc中有無 copydispose函數 (1 << 25)
  • 結構體的Desc中有無 signatureStr type encodings (char * 類型字符串) (1<<30)
  • ......

FuncPtr : 就是你定義block的內部邏輯實現函數的指針。經過編譯器把OC的代碼處理成c語言的函數後在block初始化時,用這個變量記錄函數的指針地址,當block被調用時就是執行這個函數指針指向的函數。指針

Desc. Block_size : block的內存佔用空間的大小code

Desc -> copy + dispose 函數 :block用於管理自身內存的函數orm

......cdn

更詳細的 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
複製代碼

關於類型強轉

類型強轉其實並無改變目標變量的實際內存的數據,類型強轉其實就是告訴編譯器 目標變量 是我強轉的類型數據,你對這個變量訪問時按照我指定的變量類型來訪問便可。

相關文章
相關標籤/搜索