天天看點

iOS Block的本質(一)iOS Block的本質(一)

iOS Block的本質(一)

1.對block有一個基本的認識

  • block本質上也是一個oc對象,他内部也有一個isa指針。block是封裝了函數調用以及函數調用環境的OC對象。

2.探尋block的本質

  • 首先寫一個簡單的block
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        void (^block)(int, int) =  ^(int a , int b){
            NSLog(@"this is a block! -- %d", age);
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
        };

        block(10, 10);
    }
    return 0;
}           

複制

3.檢視其内部結構

  1. 使用指令行将代碼轉化為c++與OC代碼進行比較

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

    指令代碼

    // 編譯後代碼 int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; int age = 10; void (*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10); } return 0; }

  2. 從以上c++代碼中block的聲明和定義分别與oc代碼中相對應顯示。将c++中block的聲明和調用分别取出來檢視其内部實作。
  3. 定義block變量

    代碼

    // 定義block變量代碼 void(*block)(int ,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); // 可以簡化為下列 // void (*block)(int, int) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age);

  4. 上述定義代碼中,可以發現,block定義中調用了__main_block_impl_0函數,并且将__main_block_impl_0函數的位址指派給了block。那麼我們來看一下__main_block_impl_0函數内部結構。
  5. __main_block_imp_0結構體

    struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };

  6. __main_block_imp_0結構體内有一個同名構造函數__main_block_imp_0,構造函數中對一些變量進行了指派最終會傳回一個結構體。
    • 那麼也就是說最終将一個__main_block_imp_0結構體的位址指派給了block變量
    • __main_block_impl_0結構體内可以發現__main_block_impl_0構造函數中傳入了四個參數。(void *)__main_block_func_0、&__main_block_desc_0_DATA、age、flags。其中flage有預設值,也就說flage參數在調用的時候可以省略不傳。而最後的 age(_age)則表示傳入的_age參數會自動指派給age成員,相當于age = _age。
    • 接下來着重看一下前面三個參數分别代表什麼。
    1. (void *)__main_block_func_0

      參數

      ```C++ static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) { int age = __cself->age; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_0, age); NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_1); NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_2); NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_3); } ```

      • 在__main_block_func_0函數中首先取出block中age的值,緊接着可以看到四個熟悉的NSLog,可以發現這段代碼恰恰是我們在block塊中寫下的代碼。
      • 那麼__main_block_func_0函數中其實存儲着我們block中寫下的代碼。而__main_block_impl_0函數中傳入的是(void *)__main_block_func_0,也就說将我們寫在block塊中的代碼封裝成__main_block_func_0函數,并将__main_block_func_0函數的位址傳入了__main_block_impl_0的構造函數中儲存在結構體内。
    2. &__main_block_desc_0_DATA

      參數

      C++ static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

      • 我們可以看到__main_block_desc_0中存儲着兩個參數,reserved和Block_size,并且reserved指派為0而Block_size則存儲着__main_block_impl_0的占用空間大小。最終将__main_block_desc_0結構體的位址傳入__main_block_func_0中指派給Desc。
    3. age

      參數
      • age也就是我們定義的局部變量。因為在block塊中使用到age局部變量,是以在block聲明的時候這裡才會将age作為參數傳入,也就說block會捕獲age,如果沒有在block中使用age,這裡将隻會傳入(void *)__main_block_func_0,&__main_block_desc_0_DATA兩個參數。

        這裡可以根據源碼思考一下為什麼當我們在定義block之後修改局部變量age的值,在block調用的時候無法生效。

      • 因為block在定義的之後已經将age的值傳入存儲在__main_block_imp_0結構體中并在調用的時候将age從block中取出來使用,是以在block定義之後對局部變量進行改變是無法被block捕獲的。
  7. 此時回過頭來檢視__main_block_impl_0結構體

    struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp;// block 内部代碼塊位址 Desc = desc;// 存儲block 對象占用的記憶體大小 } };

  8. 首先我們看一下__block_impl第一個變量就是__block_impl結構體。

    // __block_impl結構體内部 struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; };

  9. 我們可以發現__block_impl結構體内部就有一個isa指針。是以可以證明block本質上就是一個oc對象。而在構造函數中将函數中傳入的值分别存儲在__main_block_impl_0結構體執行個體中,最終将結構體的位址指派給block。
  10. 接着通過上面對__main_block_impl_0結構體構造函數三個參數的分析我們可以得出結論:
    1. __block_impl結構體中isa指針存儲着&_NSConcreteStackBlock位址,可以暫時了解為其類對象位址,block就是_NSConcreteStackBlock類型的。
    2. block代碼塊中的代碼被封裝成__main_block_func_0函數,FuncPtr則存儲着__main_block_func_0函數的位址。
    3. Desc指向__main_block_desc_0結構體對象,其中存儲__main_block_impl_0結構體所占用的記憶體。
  11. 調用block執行内部代碼

    ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);

  12. 通過上述代碼可以發現調用block是通過block找到FunPtr直接調用,通過上面分析我們知道block指向的是__main_block_impl_0類型結構體,但是我們發現__main_block_impl_0結構體中并不直接就可以找到FunPtr,而FunPtr是存儲在__block_impl中的,為什麼block可以直接調用__block_impl中的FunPtr呢?
  13. 重新檢視上述源代碼可以發現,(__block_impl *)block将block強制轉化為__block_impl類型的,因為__block_impl是__main_block_impl_0結構體的第一個成員,相當于将__block_impl結構體的成員直接拿出來放在__main_block_impl_0中,那麼也就說明__block_impl的記憶體位址就是__main_block_impl_0結構體的記憶體位址開頭。是以可以轉化成功。并找到FunPtr成員。
  14. 上面我們知道,FunPtr中存儲着通過代碼塊封裝的函數位址,那麼調用此函數,也就是會執行代碼塊中的代碼。并且回頭檢視__main_block_func_0函數,可以發現第一個參數就是__main_block_impl_0類型的指針。也就是說将block傳入__main_block_func_0函數中,便于重中取出block捕獲的值。

3.驗證block的本質

  • 驗證block的本質是__main_block_impl_0結構體類型。
  1. 通過代碼證明一下上述内容:

    #import <Foundation/Foundation.h> struct __main_block_desc_0 { size_t reserved; size_t Block_size; }; struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; }; int main(int argc, const char * argv[]) { @autoreleasepool { int age = 10; void (^block)(int, int) = ^(int a , int b){ NSLog(@"this is a block! -- %d", age); NSLog(@"this is a block!"); NSLog(@"this is a block!"); NSLog(@"this is a block!"); }; // 将底層的結構體強制轉化為我們自己寫的結構體,通過我們自定義的結構體探尋block底層結構體 struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block; block(10, 10); } return 0; }

  2. 通過打斷點可以看出我們自定義的結構體可以被指派成功,以及裡面的值。
iOS Block的本質(一)iOS Block的本質(一)
  1. 接下來斷點來到block代碼塊中,看一下堆棧資訊中的函數調用位址。Debuf workflow -> always show Disassembly
iOS Block的本質(一)iOS Block的本質(一)
  1. 通過上圖可以看到位址确實和FuncPtr中的代碼塊位址一樣。

總結

  • block的原理是怎樣的?本質是什麼?
    • block本質上也是一個OC對象,它内部也有個isa指針
    • block是封裝了函數調用以及函數調用環境的OC對象
    • block的底層結構如下圖所示
    iOS Block的本質(一)iOS Block的本質(一)

引用