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.查看其內部結構
-
使用命令行將代碼轉化為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; }
-
從以上c++代碼中block的聲明和定義分別與oc代碼中相對應顯示。將c++中block的聲明和調用分別取出來查看其內部實現。
-
定義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);
-
上述定義代碼中,可以發現,block定義中調用了__main_block_impl_0函數,並且將__main_block_impl_0函數的地址賦值給了block。那么我們來看一下__main_block_impl_0函數內部結構。
-
__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; } };
-
__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。
- 接下來着重看一下前面三個參數分別代表什么。
-
(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的構造函數中保存在結構體內。
-
&__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的占用空間大小。最終將\_\_main\_block\_desc\_0結構體的地址傳入\_\_main\_block\_func\_0中賦值給Desc。
-
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捕獲的。
- age也就是我們定義的局部變量。因為在block塊中使用到age局部變量,所以在block聲明的時候這里才會將age作為參數傳入,也就說block會捕獲age,如果沒有在block中使用age,這里將只會傳入(void *)__main_block_func_0,&__main_block_desc_0_DATA兩個參數。
-
此時回過頭來查看__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 對象占用的內存大小 } };
-
首先我們看一下__block_impl第一個變量就是__block_impl結構體。
// __block_impl結構體內部 struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; };
-
我們可以發現__block_impl結構體內部就有一個isa指針。因此可以證明block本質上就是一個oc對象。而在構造函數中將函數中傳入的值分別存儲在__main_block_impl_0結構體實例中,最終將結構體的地址賦值給block。
-
接着通過上面對__main_block_impl_0結構體構造函數三個參數的分析我們可以得出結論:
- __block_impl結構體中isa指針存儲着&_NSConcreteStackBlock地址,可以暫時理解為其類對象地址,block就是_NSConcreteStackBlock類型的。
- block代碼塊中的代碼被封裝成__main_block_func_0函數,FuncPtr則存儲着__main_block_func_0函數的地址。
- Desc指向__main_block_desc_0結構體對象,其中存儲__main_block_impl_0結構體所占用的內存。
-
調用block執行內部代碼
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
-
通過上述代碼可以發現調用block是通過block找到FunPtr直接調用,通過上面分析我們知道block指向的是__main_block_impl_0類型結構體,但是我們發現__main_block_impl_0結構體中並不直接就可以找到FunPtr,而FunPtr是存儲在__block_impl中的,為什么block可以直接調用__block_impl中的FunPtr呢?
-
重新查看上述源代碼可以發現,(__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成員。
-
上面我們知道,FunPtr中存儲着通過代碼塊封裝的函數地址,那么調用此函數,也就是會執行代碼塊中的代碼。並且回頭查看__main_block_func_0函數,可以發現第一個參數就是__main_block_impl_0類型的指針。也就是說將block傳入__main_block_func_0函數中,便於重中取出block捕獲的值。
3.驗證block的本質
- 驗證block的本質是__main_block_impl_0結構體類型。
-
通過代碼證明一下上述內容:
#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; }
-
通過打斷點可以看出我們自定義的結構體可以被賦值成功,以及里面的值。
-
接下來斷點來到block代碼塊中,看一下堆棧信息中的函數調用地址。Debuf workflow -> always show Disassembly
-
通過上圖可以看到地址確實和FuncPtr中的代碼塊地址一樣。
總結
- block的原理是怎樣的?本質是什么?
- block本質上也是一個OC對象,它內部也有個isa指針
- block是封裝了函數調用以及函數調用環境的OC對象
- block的底層結構如下圖所示