iOS-block本質是什麼?

俊華的部落格發表於2021-06-29

 

一: block的原理是怎樣的?本質是什麼?

  • block本質上也是一個OC物件,因為它的內部也有個isa指標
  • block是封裝了函式呼叫以及函式呼叫環境的OC物件

接下來我們將通過底層原始碼來論證上訴兩點.
首先我們寫一個簡單的block,通過clang編譯器編譯成C++程式碼,檢視一下block的底層機構:

 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        int height = 10;
        //宣告block變數
        void(^myBlock)(void) = ^{
            NSLog(@"age is %d height is %d",age,height);
        };
        //呼叫block
        myBlock();
    }
    return 0;
}

 

通過clang編譯器執行編譯成C++程式碼:

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

 

clang編譯器編譯完後會得到一個.cpp格式的檔案,這就是我們剛才轉換的.m檔案的底層程式碼.我們開啟.cpp檔案,差不多有三萬多行程式碼,我們直接拖到最下面,找到block相關的程式碼.如下

一: block底層資料結構

// 一: block底層資料結構
struct __main_block_impl_0 {
    struct __block_impl impl; // 1: impl 結構體
    struct __main_block_desc_0* Desc; // 2: block描述資訊的結構體
    int age; //3:捕獲的外部變數
    int height;
    //4: 和結構體同名的建構函式 ( C++語法 , 類似於 OC 的init方法,返回一個結構體物件,類似於返回self)
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int _height, int flags=0) : age(_age), height(_height) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

 

通過底層程式碼我們可以看到,block在底層中的資料結構是一個結構體,這個結構體有四個部分組成:
1: struct __block_impl
2: struct __main_block_desc_0
3: 捕獲的外部變數
4:和block結構體同名的建構函式

 

我們找到struct __block_impl 結構體:

//struct __block_impl 結構體
struct __block_impl {
  void *isa; //指向 block 的型別
  int Flags;//按位表示block的附加資訊
  int Reserved;//保留變數
  void *FuncPtr; //封裝了執行 block 程式碼塊的函式地址
};

 

然後我們再找到struct __main_block_desc_0 結構體 :

static struct __main_block_desc_0 {
  size_t reserved;//保留變數大小
  size_t Block_size;//block所佔用的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

 

這3個結構體之間的關係就像下圖這樣:

 

 


 

我們在網上看到的一張很經典的block底層結構的圖片就是把這兩個結構體成員列表彙總進去得到的:(descriptor結構體中的copy 和 dispose 我們後面會講到)

 

 

 

二:  main函式 (block 捕獲區域性變數(auto變數)捕獲到內部 值傳遞)

// 1: main函式
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int age = 10;
        int height = 10;
        //宣告block變數
        void(*myBlock)(int ,int) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, height));
        //呼叫block
        ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    }
    return 0;
}

 

這段底層程式碼中,宣告block和呼叫block的部分涉及了太多的型別轉換,不便於閱讀理解,我們去掉型別轉換的部分,簡化如下:

// : main函式
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int age = 10;
        int height = 10;
        //宣告block
void(*myBlock)(void) = &__main_block_impl_0(//呼叫block資料結構中的同名的構造函
                                __main_block_func_0,//封裝了block要執行的程式碼塊的函式
                                &__main_block_desc_0_DATA,//block描述資訊的結構體
                                age,//把區域性變數當做引數傳入
                                height
                                );
        //呼叫block
        myBlock->FuncPtr(myBlock);
    }
    return 0;
}

 

可以看到,我們在申明block的時候,呼叫block的建構函式,傳入了四個引數,分別是__main_block_func_0 , __main_block_desc_0_DATA , age , height,我們對比著它的建構函式,看看它的內部都做了什麼:

 
// block底層資料結構
struct __main_block_impl_0 {
    struct __block_impl impl; // 1: impl 結構體
    struct __main_block_desc_0* Desc; // 2: block描述資訊的結構體
    int age;//3: 捕獲的外部變數
    int height;
    //4: 同名的建構函式 ( C++語法 )
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int _height, int flags=0) : age(_age), height(_height)  {
    impl.isa = &_NSConcreteStackBlock; //isa 指向了block的型別
    impl.Flags = flags;//按位表示block的一些附加資訊,這裡是0
    impl.FuncPtr = fp; // 封裝了block要執行的程式碼塊的函式
    Desc = desc; //block描述資訊的結構體
  }
};
  • 首先,建構函式的內部會給impl結構體的isa指標賦值,決定block的型別;
  • 然後,再把傳進來的__main_block_func_0傳給了fp,又把fp賦值給了impl結構體的FuncPtr,__main_block_func_0這個函式指標指向封裝了block程式碼塊的函式地址;
  • 最後,再把傳進來的__main_block_desc_0_DATA結構體賦值給了Desc;
    其實這裡還隱藏了一個很重要的一步,只是底層程式碼上沒有體現出來:在建構函式的內部,會自動把傳遞進來_age,_height自動賦值給我們block內部的age,height,這就是block的捕獲機制.

我們來分別看一下__main_block_func_0,__main_block_desc_0_DATA這兩個結構體長什麼樣子:

  • __main_block_func_0 結構體: block 真正要執行的程式碼塊
// 封裝了block要執行的程式碼塊的函式
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {

 NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_b680e8_mi_0,a,b);

        }

 

可以看到,就是我們.m中寫的很簡單的一句NSLog語句.說明block的程式碼塊確實是封裝成了一個函式去執行的.

  • __main_block_desc_0_DATA:
 
static struct __main_block_desc_0 {
  size_t reserved;//保留變數的大小
  size_t Block_size;//block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

 

這個結構體中主要就是一個size_t Block_size 存放了block的大小.

 

三、總結:

  • block的底層就是一個struct _block_impl_0型別的結構體,這個結構體中包含一個isa指標,所以從更深的層次來說,block就是一個OC物件.

  • block結構體中又包含implDesc結構體,impl結構體中有一個非常重要的成員FuncPtr,FuncPtr是一個指標,指向了封裝了blcok程式碼塊的函式,
    我們看到呼叫block的程式碼:myBlock()的底層實際上是這樣子:myBlock->FuncPtr(myBlock);,可以看出呼叫block其實就是呼叫了FuncPtr()這個函式

  • Desc結構體存放了block的大小


參考連結:https://www.jianshu.com/p/e6759404f9cd

相關文章