探究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
從上圖中,我們可以清晰的看到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定義時,傳入的兩個引數
那我們再來看下,傳入的這兩個引數又是什麼東西
__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定義所傳的引數
從圖中我們可以看出,__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
通過上面的圖,__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通過強制轉換成_main_block_impl_0後,我們可以在結構體中看到num被賦值了10,那麼也就驗證了block的本質其實就是_main_block_impl_0這個結構體物件
上面已經驗證了block的本質,那麼接下來我們繼續驗證FuncPtr存放著bloc程式碼塊的地址
我們繼續將斷點執行到block的程式碼塊中,通過Debug->Debuf workflow -> always show Disassembly檢視地址
通過上圖和上上圖中的FuncPtr地址一對比,確實相同的,這樣也就驗證了FuncPtr存放著block程式碼塊的地址總結
1.block本質上也是一個OC物件,它內部有一個isa指標
2.block中封裝著block程式碼塊的地址,block的大小,需要使用的變數