Block學習①--block的本質

潘小懶同學發表於2018-07-08

探究block的本質

我們通過一個簡單的block來展開我們的探究

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void)= ^{
            NSLog(@"hello world");
        };
        block();
    }
    return 0;
}
複製程式碼

使用命令列將上述程式碼轉化成C++程式碼,來檢視其內部的實現

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

Block學習①--block的本質

從上圖中,我們可以清晰的看到C++程式碼與OC程式碼之間的對比,我們將從C++程式碼中一步步來分析block的本質

定義block

void(*block)(void)= ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

//刪除強制轉換的內容,簡化程式碼
void(*block)(void)= &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
複製程式碼

上述程式碼,就是OC定義block變數轉C++的程式碼,我們可以發現,在block的定義程式碼中,block得到的是__main_block_impl_0的地址,而__main_block_impl_0是一個函式,呼叫該函式,需要傳遞兩個值,這兩個值有什麼作用,我們就進一步看看__main_block_impl_0的內部結構是什麼樣子的

__main_block_impl_0

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //建構函式(類似OC中的init方法),返回結構體物件
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製程式碼

可以看出__main_block_impl_0是一個結構體,這個結構體當中,我們可以發現,存在著一個與__main_block_impl_0同名的建構函式,建構函式中給一些變數進行了賦值,返回結構體物件。這樣我們暫時可以得出一個結論,block的本質其實指向的是一個結構體物件

在這個建構函式當中,我們可以發現傳入了三個引數,而其中的flags設定了預設值,那就意味著flags在呼叫時,可以忽略不傳,這樣的話,我們就可以對比一下在block定義時,傳入的兩個引數

Block學習①--block的本質

那我們再來看下,傳入的這兩個引數又是什麼東西

__main_block_func_0

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_71_y1769s116jb_1c73scvvmy300000gn_T_main_81828d_mi_0);
        }
複製程式碼

當我們看到NSLog的時候,我們大概就可以猜到,__main_block_func_0其實就是封裝了block執行邏輯的函式,我們再綜合上面所說的block定義所傳的引數

Block學習①--block的本質
從圖中我們可以看出,__main_block_func_0函式是將自己的地址傳遞給我了__main_block_impl_0函式,而__main_block_impl_0中impl.FuncPtr存放這該地址,那麼我們就可以得到結論,FuncPtr存放的是執行程式碼塊的地址

__main_block_desc_0_DATA

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的佔用記憶體的大小,在前面我們已經說了block的本質其實就是__main_block_impl_0這個結構體,那麼Block_size儲存的其實就是block的佔用記憶體大小

簡化__main_block_impl_0

Block學習①--block的本質

通過上面的圖,__main_block_impl_0包含著兩個東西,__main_block_desc_0我們就不用再說了,它存在著block的佔用記憶體大小,再看struct __block_impl,這個結構體中包含著isa指標,FuncPtr,既然包含著isa,那我們是否也可以認為block本質上就是一個OC物件。

通過探究__main_block_impl_0這個結構體,我們大致可以得出以下結論

1.block本質上其實指向的是一個結構體物件,而這個結構體就是_main_block_impl_0

2. block的執行邏輯函式被封裝成了__main_block_func_0函式

3.__block_impl中存在isa指標,那麼block本質上是一個OC物件,而它當中的FuncPtr則存放著__main_block_func_0函式的地址

呼叫block

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

//程式碼簡化
block->FuncPtr(block);
複製程式碼

通過上述的簡化程式碼可以發現block的呼叫其實是通過FuncPtr來找到block的執行程式碼塊,上面我們已經說過,FuncPtr存放的是block執行程式碼塊的地址,但是block是如何拿到FuncPtr的,而且我們檢視block指向的__main_block_impl_0結構體中並不存在FuncPtr

我們再一次回到呼叫block的原始碼中檢視

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)
複製程式碼

我們可以看出block被__block_impl *強制轉化成__block_impl,而__block_impl結構體中存在著FuncPtr

通過上面這麼多的內容講述,我們其實並沒怎麼通過程式碼去驗證一些東西,比如,block的本質真的是__main_block_impl_0這個結構體嗎?FuncPtr真的是存放著block的執行程式碼塊的地址嗎?接下來我們將一步步的通過程式碼來證明

block的本質其實就是_main_block_impl_0結構體

我們將原始碼中_main_block_impl_0的一些引數放到OC程式碼中,並且在_main_block_impl_0中加入一個變數num來進行驗證

Block學習①--block的本質

然後我們通過打斷點來進行檢視

Block學習①--block的本質

上圖我們可以很清楚的看見,block通過強制轉換成_main_block_impl_0後,我們可以在結構體中看到num被賦值了10,那麼也就驗證了block的本質其實就是_main_block_impl_0這個結構體物件

上面已經驗證了block的本質,那麼接下來我們繼續驗證FuncPtr存放著bloc程式碼塊的地址

我們繼續將斷點執行到block的程式碼塊中,通過Debug->Debuf workflow -> always show Disassembly檢視地址

Block學習①--block的本質
通過上圖和上上圖中的FuncPtr地址一對比,確實相同的,這樣也就驗證了FuncPtr存放著block程式碼塊的地址

總結

1.block本質上也是一個OC物件,它內部有一個isa指標

2.block中封裝著block程式碼塊的地址,block的大小,需要使用的變數

參考:iOS底層原理總結 - 探尋block的本質(一)

相關文章