
本篇學(xué)習(xí)內(nèi)容



block的本質(zhì)
block的原理是怎樣的?本質(zhì)是什么?
- block本質(zhì)上也是一個OC對象,因為它的內(nèi)部也有個isa指針
- block是封裝了函數(shù)調(diào)用以及函數(shù)調(diào)用環(huán)境的OC對象
通俗的理解:block就是將一些代碼封裝起來,以便在將來某個時候被使用,如果你不去調(diào)用block,block內(nèi)部封裝的代碼就不會執(zhí)行。
舉一個簡單的例子
int main(int argc, const char * argv[]) {
@autoreleasepool {
^{
NSLog(@"this is a block!");
NSLog(@"this is a block!");
NSLog(@"this is a block!");
NSLog(@"this is a block!");
};
}
return 0;
}
RUN>
沒有任何輸出,Block代碼塊沒調(diào)用
Block的使用也很簡單,可以像函數(shù)一樣被使用。加上()就代表調(diào)用,如下
int main(int argc, const char * argv[]) {
@autoreleasepool {
^{
NSLog(@"this is a block!");
NSLog(@"this is a block!");
NSLog(@"this is a block!");
NSLog(@"this is a block!");
}();
}
return 0;
}
RUN>
2021-04-22 15:35:54.161606+0800 Interview03-block[4322:148955] this is a block! 2021-04-22 15:35:54.162084+0800 Interview03-block[4322:148955] this is a block! 2021-04-22 15:35:54.162138+0800 Interview03-block[4322:148955] this is a block! 2021-04-22 15:35:54.162160+0800 Interview03-block[4322:148955] this is a block!
block的底層結(jié)構(gòu)-block的本質(zhì)探索
寫個簡單的block,其中block內(nèi)部使用了block外部的age變量:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 20;
void (^block)(int, int) = ^(int a , int b){
NSLog(@"this is a block! -- %d", age);
};
block(10, 10);
}
return 0;
}
RUN>
2021-04-22 15:38:10.449976+0800 Interview03-block[4340:150550] this is a block! -- 20上面的代碼可以看出,block里面使用了它上面的
int age = 20,可以將這個先簡單的理解成函數(shù)調(diào)用環(huán)境,顧名思義,就是block所用到的一些外部變量。
通過clang編譯器執(zhí)行編譯成C++代碼:
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
clang編譯器編譯完后會得到一個.cpp格式的文件,這就是我們剛才轉(zhuǎn)換的.m文件的底層代碼.
main函數(shù)的C++代碼如下
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 20;
//block底層定義
void (*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
//block底層調(diào)用
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
}
return 0;
}
但是由于底層的代碼添加了許多強轉(zhuǎn),我們簡化代碼,如下:
//block底層定義
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);
//block底層調(diào)用
block->FuncPtr(block, 10, 10);
block底層定義
//block底層定義
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);
先看__main_block_impl_0這個函數(shù),我們發(fā)現(xiàn)它被定義在一個同名結(jié)構(gòu)體里面,這個__main_block_impl_0結(jié)構(gòu)體就是block的底層實現(xiàn)
// 一: block底層數(shù)據(jù)結(jié)構(gòu)
struct __main_block_impl_0 {
struct __block_impl impl; // 1: impl 結(jié)構(gòu)體
struct __main_block_desc_0* Desc; // 2: block描述信息的結(jié)構(gòu)體
int age; //3:捕獲的外部變量
//4: 和結(jié)構(gòu)體同名的構(gòu)造函數(shù) ( C++語法 , 類似于 OC 的init方法,返回一個結(jié)構(gòu)體對象,類似于返回self)
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age){
impl.isa = &_NSConcreteStackBlock;//isa指向類對象,就比如str的isa指向NSString
impl.Flags = flags;
impl.FuncPtr = fp;//外面的__main_block_func_0函數(shù)地址傳進(jìn)來,保存在這里
Desc = desc;//外面的__main_block_desc_0結(jié)構(gòu)體地址傳進(jìn)來,保存在這里
}
};
通過底層代碼我們可以看到,block在底層中的數(shù)據(jù)結(jié)構(gòu)是一個結(jié)構(gòu)體,這個結(jié)構(gòu)體有四個部分組成:
1: struct __block_impl
2: struct __main_block_desc_0
3: 捕獲的外部變量
4:和block結(jié)構(gòu)體同名的構(gòu)造函數(shù)
我們找到struct __block_impl 結(jié)構(gòu)體:
//struct __block_impl 結(jié)構(gòu)體
struct __block_impl {
void *isa; //指向 block 的類型
int Flags;//按位表示block的附加信息
int Reserved;//保留變量
void *FuncPtr; //封裝了執(zhí)行 block 代碼塊的函數(shù)地址
};
發(fā)現(xiàn)這個結(jié)構(gòu)體里面第一個成員就是isa,驗證了block本質(zhì)上也是一個OC對象。
然后我們再找到struct __main_block_desc_0 結(jié)構(gòu)體 :
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)};
可以發(fā)現(xiàn),這個結(jié)構(gòu)體被重新命名為__main_block_desc_0_DATA,默認(rèn)傳入了兩個值0和sizeof(struct __main_block_impl_0),block的底層就是__main_block_impl_0結(jié)構(gòu)體,所以這個結(jié)構(gòu)體==第二個值保存的是block的大小==。
接下來我們看一下__main_block_impl_0函數(shù)的參數(shù),第一個參數(shù)是指向__main_block_func_0函數(shù)的指針,如下:
//封裝了block執(zhí)行邏輯的函數(shù)
//第一個參數(shù)是block,后面是block調(diào)用的時候傳入的參數(shù)
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);
}
現(xiàn)在我們知道了,首先__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age)函數(shù)有兩個參數(shù),第一個參數(shù)是__main_block_func_0函數(shù)的地址(這個函數(shù)里面封裝了我們block里面執(zhí)行的代碼),第二個參數(shù)是__main_block_desc_0結(jié)構(gòu)體的地址(這個結(jié)構(gòu)體里面有保存block的大?。?,構(gòu)造函數(shù)的返回值是個__main_block_impl_0結(jié)構(gòu)體,block底層就是__main_block_impl_0結(jié)構(gòu)體,最后再獲取__main_block_impl_0結(jié)構(gòu)體的地址,賦值給左邊的“block”變量,然后我們拿到“block”變量就可以做其他事情了,至此,block定義完成。
block底層調(diào)用
//block底層調(diào)用
block->FuncPtr(block, 10, 10);
這句代碼就很簡單了,直接取出block里面的FuncPtr函數(shù),傳入?yún)?shù)進(jìn)行調(diào)用。
不應(yīng)該是通過“block-> impl->FuncPtr(block, 10, 10)”來拿到FuncPtr嗎?其實我們在簡化之前,代碼是這樣的:
((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
可以發(fā)現(xiàn)系統(tǒng)把block強轉(zhuǎn)成__block_impl類型的了,由于impl又是__main_block_impl_0結(jié)構(gòu)體的第一個成員,所以impl的地址和__main_block_impl_0結(jié)構(gòu)體的地址是一樣的,強轉(zhuǎn)之后可以直接獲取到FuncPtr。
根據(jù)如上分析,驗證了,block是封裝了函數(shù)調(diào)用以及函數(shù)調(diào)用環(huán)境的OC對象。
__main_block_impl_0``__block_impl與__main_block_desc_03個結(jié)構(gòu)體之間的關(guān)系

咋們通過梳理了block底層幾個類的關(guān)系,現(xiàn)在調(diào)整一下代碼
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 = 20;
void (^block)(int, int) = ^(int a , int b){
NSLog(@"this is a block! -- %d", age);
};
struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;
block(10, 10);
}
return 0;
}
加斷點,驗證一下內(nèi)存

FuncPtr地址驗證

總結(jié):
如上圖所示,block底層就是一個__main_block_impl_0結(jié)構(gòu)體,它由三個部分組成:
- 第一部分是impl,它是個結(jié)構(gòu)體,里面有isa指針和FuncPtr指針,F(xiàn)uncPtr指針指向__main_block_func_0函數(shù),這個函數(shù)里面封裝了block需要執(zhí)行的代碼。
- 第二部分是desc,它是個指針,指向__main_block_desc_0結(jié)構(gòu)體,它里面有一個Block_size用來保存block的大小。
- 第三部分是age,它把外面訪問的成員變量age封裝到自己里面了。
block的變量捕獲(capture)

- 如果是被auto修飾的局部變量,會被捕獲,是值傳遞
- 如果是被static修飾的局部變量,會被捕獲,是指針傳遞
- 如果是全局變量,不會被捕獲,因為可以直接訪問
一:auto變量
- auto變量:自動變量,離開作用域就會銷毀,一般我們創(chuàng)建的局部變量都是auto變量,比如
int age = 10,系統(tǒng)會在默認(rèn)在前面加上auto int age = 10
首先我們要搞清楚,什么是捕獲,所謂捕獲外部變量,意思就是在block內(nèi)部,創(chuàng)建一個變量來存放外部變量,這就叫做捕獲.
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
void (^block)(void) = ^{
NSLog(@"age is %d",age);
};
age = 20;
block();
}
return 0;
}
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
{
int age = 10;
//定義block
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
age
);
age = 20;
//調(diào)用block
block->FuncPtr(block);
return 0;
}
我們看到在調(diào)用block的構(gòu)造函數(shù)時,傳入了三個參數(shù),分別是:__main_block_func_0,&__main_block_desc_0_DATA,age,我們找到block的構(gòu)造函數(shù),看看內(nèi)部如何處理這個age:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age; // 1: 定義了一個同名的age變量
//block構(gòu)造函數(shù)
__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;
age = _age; //2 :C++的特殊語法,在構(gòu)造函數(shù)內(nèi)部會默認(rèn)把_age賦值給age
}
};
通過查看block的內(nèi)部結(jié)構(gòu)看我們發(fā)現(xiàn),block內(nèi)部創(chuàng)建了一個age變量,并且在block構(gòu)造函數(shù)中,把傳遞進(jìn)來的_age賦值給了這個age變量.我們看看調(diào)用block時,他的底部取的是哪個age:
//調(diào)用block的FuncPtr函數(shù),把block當(dāng)做參數(shù)傳遞進(jìn)去
block->FuncPtr(block);
//FuncPtr函數(shù)內(nèi)部
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//通過傳遞的block,找到block內(nèi)部的age
int age = __cself->age; // bound by copy
//打印age
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_3089d7_mi_0,age);
}
通過底層代碼,我們看到,在調(diào)用block時,block會找到自己內(nèi)部的age變量,然后打印數(shù)出,所以我們修改age = 20,并不會影響block內(nèi)部的age值
二:static變量
我們執(zhí)行如下代碼:
int main(int argc, const char * argv[]) {
@autoreleasepool {
auto int age = 10;
static int height = 10;
void (^block)(void) = ^{
// age的值捕獲進(jìn)來
// height的指針捕獲進(jìn)來
NSLog(@"age is %d, height is %d", age, height);
};
age = 20;
height = 20;
block();
}
return 0;
}
RUN>
2021-04-22 16:31:33.658732+0800 Interview01-Block的本質(zhì)[4670:177646] age is 10, height is 20
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
代碼轉(zhuǎn)成C++代碼,抽取關(guān)鍵的代碼,如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *height;
__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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *height = __cself->height; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2w_t9gvrhjs7gv_m4kb_8q3r_980000gn_T_main_12eb7d_mi_1, age, (*height));
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
auto int age = 10;
static int height = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));//&height傳遞指針
age = 20;
height = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
可以看出age和height都被捕獲了,age是值捕獲,height是指針捕獲。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;//定義 age 變量
int *height;//定義一個 指針變量,存放外部變量的指針
__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;
}
};
定義了兩個變量age,height,不同的是,height是一個指針指針變量,用于存放外部變量的指針.我們再來看看執(zhí)行block代碼塊的內(nèi)部:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *height = __cself->height; // bound by copy
// *height : 取出指針變量所指向的內(nèi)存的值
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_bf6cae_mi_0,age,(*height));
}
我們看到,對于age是捕獲到內(nèi)部,把外部age的值存起來,而對于height,是把外部變量的指針保存起來,所以,我們在修改height時,會影響到block內(nèi)部的值
- 我們定義block,其實就是初始化__main_block_impl_0結(jié)構(gòu)體,定義block的時候會把age和&height傳進(jìn)去,這個結(jié)構(gòu)體里面有一個age一個height指針用于接收傳進(jìn)去的值,這行代碼“age(_age), height(_height)”就是保證外面的變量改變的時候?qū)崟r改變結(jié)構(gòu)體里面的age和height指針的值。
- 我們調(diào)用block的時候,其實就是執(zhí)行__main_block_func_0函數(shù),這個函數(shù)會獲取__main_block_impl_0結(jié)構(gòu)體中age和height指針的值,所以打印的時候就會把age和*height的值打印出來。
- 所以執(zhí)行完block之后age的值沒改變,因為是值傳遞,height的值改變了,因為是指針傳遞。
為什么auto變量是值傳遞,static變量是指針傳遞呢?
因為auto是自動變量,出了作用域后會自動銷毀的,如果我們保留他的指針,就會存在訪問野指針的情況
//定義block類型
void(^block)(void);
void test(){
int age = 10;
static int height = 20;
//在block內(nèi)部訪問 age , height
block = ^{
NSLog(@"age is %d, height is %d",age,height);
};
age = 20;
height = 20;
}
//在main函數(shù)中調(diào)用
int main(int argc, const char * argv[]) {
test();
//test調(diào)用后,age變量就會自動銷毀,如果block內(nèi)部是保留age變量的指針,那么我們在調(diào)用block()時,就出現(xiàn)訪問野指針
block();
}
三:全局變量
全局變量哪里都可以訪問,所以block內(nèi)部是不會捕獲全局變量的,直接訪問
int age_ = 10;
static int height_ = 10;
void (^block)(void);
void test()
{
block = ^{
// age的值捕獲進(jìn)來(capture)
NSLog(@"age is %d, height is %d", age, height);
};
age = 20;
height = 20;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
轉(zhuǎn)成C++文件之后,代碼如下:
int age_ = 10;
static int height_ = 10;
void (*block)(void);
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yh_qjzhl57s63j2m9l4frv27zjc0000gn_T_main_478a1b_mi_0, age, height);
}
static struct __test_block_desc_0 {
size_t reserved;
size_t Block_size;
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)};
void test()
{
block = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA));
age = 20;
height = 20;
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
test();
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}

為什么全局變量不需要捕獲?
因為全局變量無論哪個函數(shù)都可以訪問,
block內(nèi)部當(dāng)然也可以正常訪問,所以根本無需捕獲
為什么局部變量就需要捕獲呢?
因為作用域的問題,我們在一個函數(shù)中定義變量,在
block內(nèi)部訪問,本質(zhì)上跨函數(shù)訪問,所以需要捕獲起來.
四:self的捕獲
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
@property (copy, nonatomic) NSString *name;
- (void)test;
- (instancetype)initWithName:(NSString *)name;
@end
#import "MJPerson.h"
@implementation MJPerson
- (void)test
{
void (^block)(void) = ^{
NSLog(@"-------%d", [self name]);
};
block();
}
- (instancetype)initWithName:(NSString *)name
{
if (self = [super init]) {
self.name = name;
}
return self;
}
@end
將MJPerson.m轉(zhuǎn)成C++代碼:
struct __MJPerson__test_block_impl_0 {
struct __block_impl impl;
struct __MJPerson__test_block_desc_0* Desc;
MJPerson *self;
__MJPerson__test_block_impl_0(void *fp, struct __MJPerson__test_block_desc_0 *desc, MJPerson *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __MJPerson__test_block_func_0(struct __MJPerson__test_block_impl_0 *__cself) {
MJPerson *self = __cself->self; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_MJPerson_1027e6_mi_0, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")));
}
static void _I_MJPerson_test(MJPerson * self, SEL _cmd) {
void (*block)(void) = ((void (*)())&__MJPerson__test_block_impl_0((void *)__MJPerson__test_block_func_0, &__MJPerson__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
static instancetype _I_MJPerson_initWithName_(MJPerson * self, SEL _cmd, NSString *name) {
if (self = ((MJPerson *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MJPerson"))}, sel_registerName("init"))) {
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setName:"), (NSString *)name);
}
return self;
}

可以看出self被捕獲了,并且是指針捕獲,既然被捕獲,就說明self是局部變量。
為什么self是局部變量呢?
其實每個方法都兩個隱式參數(shù),一個是self一個是_cmd,self是方法調(diào)用者,_cmd是方法名,既然self被當(dāng)做參數(shù)了,那self肯定是局部變量了,也可以在上面的代碼中進(jìn)行驗證,我們看一下轉(zhuǎn)換后的test()方法:
static void _I_Person_test(Person * self, SEL _cmd) {
void(*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
OC 中的test()方法時沒有參數(shù)的,但是轉(zhuǎn)換成 C++ 后就多了兩個參數(shù)self,_cmd,其實我們每個 OC 方法都會默認(rèn)有這兩個參數(shù),這也是為什么我們在每個方法中都能訪問self和_cmd,而參數(shù)就是局部變量,所以block就自然而然的捕獲了self.
對于[self name],在上面的代碼可以看出是給self發(fā)送消息,如下:
objc_msgSend((id)self, sel_registerName("name"))
所以,block會捕獲self,如果想要訪問self中的成員變量就給self發(fā)送消息就好了(self都被捕獲了,肯定可以獲取到self中的其他信息了)。
總結(jié):
一:只要是局部變量,不管是auto 變量,還是static 變量,block都會捕獲.不同的是,對于auto 變量,block是保存值,而static 變量 是保存的指針.
二:如果是全局變量,根本不需要捕獲,直接訪問。
特別備注
本系列文章總結(jié)自MJ老師在騰訊課堂iOS底層原理班(下)/OC對象/關(guān)聯(lián)對象/多線程/內(nèi)存管理/性能優(yōu)化,相關(guān)圖片素材均取自課程中的課件。如有侵權(quán),請聯(lián)系我刪除,謝謝!