面試題引發(fā)的思考:
Q: block的原理是怎樣的?本質(zhì)是什么?
- block本質(zhì)上也是一個OC對象,它內(nèi)部也有一個
isa指針。 - block是封裝了 函數(shù)調(diào)用 以及 函數(shù)調(diào)用環(huán)境 的OC對象。
1. block原理
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
void (^block)(int, int) = ^(int a, int b){
NSLog(@"this is block, a = %d, b = %d", a, b);
NSLog(@"this is block, age = %d", age);
};
age = 20;
block(1, 2);
}
return 0;
}
使用命令行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m將代碼轉(zhuǎn)化成C++語言:

將C++中強制轉(zhuǎn)換代碼去掉,以便閱讀:

(1) 定義block
代碼顯示:
- 定義block時調(diào)用了
__main_block_impl_0函數(shù),并將__main_block_impl_0函數(shù)地址賦值給了block。
即:block底層就是__main_block_impl_0結(jié)構(gòu)體。
通過__main_block_impl_0找到__main_block_impl_0函數(shù):

代碼顯示:
__main_block_imp_0結(jié)構(gòu)體的第一個成員是__block_impl結(jié)構(gòu)體變量,__block_impl結(jié)構(gòu)體的第一個成員是isa;
即:__main_block_imp_0結(jié)構(gòu)體第一個成員是isa,也就是說block的底層就是一個OC對象。
__main_block_imp_0結(jié)構(gòu)體內(nèi)部有一個同名構(gòu)造函數(shù)__main_block_imp_0,會對相關(guān)變量賦值并返回一個__main_block_imp_0結(jié)構(gòu)體,然后將結(jié)構(gòu)體的地址賦值給block。
定義block時__main_block_impl_0函數(shù)傳入的3個參數(shù):
1> _main_block_func_0參數(shù):

很明顯,其中的NSLog(...);即我們寫的NSLog(@"a = %d, b = %d, age = %d", a, b, age);語句;
即:_main_block_func_0函數(shù)把Block中要執(zhí)行的代碼封裝到其內(nèi)部。
2> &_main_block_desc_0_DATA參數(shù):

__main_block_desc_0中包含兩個參數(shù):
a> reserved:賦值為0;
b> Block_size:存儲__main_block_impl_0占用的空間大小。
3> age參數(shù):
age是我們定義的局部變量。block中使用age,所以block會在聲明的時候?qū)?code>age作為參數(shù)傳入,即block會捕獲age。
因為block在定義時將age值傳入存儲在__main_block_impl_0結(jié)構(gòu)體中,并在調(diào)動block時將age取出來使用,所以在block定義結(jié)束后對局部變量進行修改是無法被block捕獲的。所以文章開始的代碼中輸出的age值為10,而非20。
下面再看__main_block_impl_0函數(shù):

由以上分析可知:
__block_impl結(jié)構(gòu)體中isa指針存儲著_NSConcreteStackBlock地址;_main_block_func_0函數(shù)把Block中要執(zhí)行的代碼封裝到其內(nèi)部,FuncPtr則存儲著__main_block_func_0函數(shù)的地址;Desc指向__main_block_desc_0結(jié)構(gòu)體對象,其中存儲__main_block_impl_0結(jié)構(gòu)體所占用的內(nèi)存。
(2) 調(diào)用block
// 調(diào)用block內(nèi)部的代碼
// 簡化版:block->FuncPtr(block, 1, 2);
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
通過__main_block_impl_0函數(shù)結(jié)構(gòu)可知:
FuncPtr是_main_block_impl_0第一個成員變量impl的成員變量,但是block調(diào)用時卻直接通過block找到FuncPtr進行調(diào)用,這是為什么呢?
原因在于:(__block_impl *)block將block強制轉(zhuǎn)化為__block_impl類型,而impl是__main_block_impl_0結(jié)構(gòu)體的第一個成員,所以impl和__main_block_impl_0的首地址是一樣的,因此指向_main_block_impl_0的首地址的指針也就可以被強制轉(zhuǎn)換為指向impl的首地址的指針,并找到FuncPtr。
而FuncPtr則存儲著__main_block_func_0函數(shù)的地址,因此通過block->FuncPtr()獲取__main_block_func_0的地址,對其進行調(diào)用,進而執(zhí)行block中的代碼。并且_main_block_func_0函數(shù)第一個參數(shù)就是__main_block_impl_0類型的指針,也就是說將block傳入__main_block_func_0函數(shù)中,進而取出block捕獲的值。
(3) 總結(jié)
通過以上分析,我們對block底層結(jié)構(gòu)有了基本的了解,由此可以分析出其中的結(jié)構(gòu)關(guān)系:

2. block的變量捕獲
為了保證block內(nèi)部能夠正常訪問外部的變量,block有個變量捕獲機制(capture)。
// 全局變量c, d
int c = 30;
static int d = 40;
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 局部變量a, b
// auto變量是聲明在函數(shù)內(nèi)部的變量,離開作用域就銷毀
// int a = 10; 則a為auto變量,會自動在前面添加auto關(guān)鍵字
auto int a = 10;
static int b = 20;
void (^block)(void) = ^{
NSLog(@"a = %d, b = %d, c = %d, d = %d", a, b, c, d);
};
a = 1;
b = 2;
c = 3;
d = 4;
block();
}
return 0;
}
// 打印結(jié)果
Demo[1234:567890] a = 10, b = 2, c = 3, d = 4
OC代碼轉(zhuǎn)化為C++查看變量調(diào)用方式:

(1) 局部變量
1> auto變量:
auto變量是聲明在函數(shù)內(nèi)部的變量,離開作用域就銷毀,局部變量前面自動添加auto關(guān)鍵字。auto變量會捕獲到block內(nèi)部,即block內(nèi)部會專門新增加一個參數(shù)來存儲變量的值。auto變量只存在于局部變量中,訪問方式為值傳遞。
2> static變量:
static變量在變量的作用域結(jié)束時并不會被系統(tǒng)自動回收static變量會捕獲到block內(nèi)部,即block內(nèi)部會專門新增加一個參數(shù)來存儲變量的值的地址。static變量只存在于局部變量中,訪問方式為地址傳遞。
(2) 全局變量
全局變量在哪里都可以訪問,所以block不用捕獲全局變量,直接進行訪問。
(3) 總結(jié)

- block處理方式不同是由變量的聲明周期決定的;
- 局部變量都會被block捕獲,
auto變量值傳遞,static變量指針傳遞;- 全局變量不會被block捕獲,直接訪問。
Q: 那么以下情況block是否會捕獲變量呢?
#import "Person.h"
@implementation Person
- (void)test {
void (^block)(void) = ^{
NSLog(@"-------- %p", self);
};
block();
}
@end
OC代碼轉(zhuǎn)化為C++查看變量調(diào)用方式:

由圖可知:self會被block捕獲。
因為OC方法會默認傳遞兩個參數(shù)self和_cmd,兩者都是局部變量,與我們的結(jié)論:局部變量會被block捕獲符合。
3. block類型
前文可知:block的本質(zhì)就是一個OC對象,所以block有類型。
下面我們探尋一下block的類型,首先關(guān)閉ARC:

int main(int argc, const char * argv[]) {
@autoreleasepool {
// __NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
void (^block)(void) = ^{
NSLog(@"Hello");
};
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
}
return 0;
}
// 打印結(jié)果
Demo[1234:567890] __NSGlobalBlock__
Demo[1234:567890] __NSGlobalBlock
Demo[1234:567890] NSBlock
Demo[1234:567890] NSObject
由打印結(jié)果可知:
block最終繼承自NSBlock,而NSBlock繼承自NSObject,所以block的isa指針是來自于NSObject。也印證了block的本質(zhì)就是OC對象。
(1) block的類型及存放區(qū)域:

- block有三種類型:
__NSGlobalBlock__、__NSMallocBlock__、__NSStackBlock__- 數(shù)據(jù)段中的
__NSGlobalBlock__直到程序結(jié)束才會被回收;- 堆中的
__NSMallocBlock__需要手動內(nèi)存管理;- 棧中的
__NSStackBlock__作用域執(zhí)行完畢被回收。
(2) block類型總結(jié):
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1. __NSGlobalBlock__:沒有訪問auto變量
void (^block1)(void) = ^{
NSLog(@"block1");
};
NSLog(@"%@", [block1 class]);
// 2. __NSStackBlock__:訪問了auto變量
int age = 10;
void (^block2)(void) = ^{
NSLog(@"block2 - %d", age);
};
NSLog(@"%@", [block2 class]);
// 3. __NSMallocBlock__:__NSStackBlock__調(diào)用了copy
NSLog(@"%@", [[block2 copy] class]);
}
return 0;
}
// 打印結(jié)果
Demo[1234:567890] __NSGlobalBlock__
Demo[1234:567890] __NSStackBlock__
Demo[1234:567890] __NSMallocBlock__
輸出打印結(jié)果可知block的三種類型:
上述代碼轉(zhuǎn)化為C++去查看源碼發(fā)現(xiàn)三個block的isa指針全部都指向_NSConcreteStackBlock類型地址。原因是runtime會對其類型進行了轉(zhuǎn)變,所以以runtime運行時類型即打印出的類型為準。
總結(jié)可得:
| block類型 | 內(nèi)存區(qū)域 | 環(huán)境 | 復(fù)制效果copy
|
|---|---|---|---|
NSGlobalBlock |
數(shù)據(jù)段 | 沒有訪問auto變量 |
什么也不做,類型不變 |
NSStackBlock |
棧 | 訪問了auto變量 |
從棧復(fù)制到堆,類型改變?yōu)?code>__ NSMallocBlock__ |
NSMallocBlock |
堆 |
__ NSStackBlock__調(diào)用copy
|
引用計數(shù)增加,類型不變 |
棧中的__NSStackBlock__訪問auto變量,作用域執(zhí)行完畢被回收,如果調(diào)用block時已經(jīng)銷毀其內(nèi)存,就會出現(xiàn)問題:
void (^block)(void);
void test() {
// __NSStackBlock__:訪問了auto變量
int age = 10;
block = ^{
NSLog(@"age = %d", age);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
// 打印結(jié)果
Demo[1234:567890] age = -272632568
由打印結(jié)果可知:
age沒有打印出正確結(jié)果,這是因為__NSStackBlock__類型的block存儲在棧中,test函數(shù)執(zhí)行完畢后,棧內(nèi)存中block所占用的內(nèi)存被系統(tǒng)回收。
我們可以通過copy將NSStackBlock類型的block轉(zhuǎn)化為NSMallocBlock類型的block
void (^block)(void);
void test() {
// __NSStackBlock__ 調(diào)用copy轉(zhuǎn)化為 __NSMallocBlock__
int age = 10;
block = [^{
NSLog(@"age = %d", age);
} copy];
[block release];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
// 打印結(jié)果
Demo[1234:567890] age = 10
- MRC環(huán)境下,經(jīng)常使用
copy將棧上的block拷貝到堆中,然后手動調(diào)用release操作將其銷毀即可;- ARC環(huán)境下,編譯器會自動將棧上的block進行
copy操作,將block復(fù)制到堆上。
4. ARC幫我們做了什么?
在ARC環(huán)境下,編譯器會根據(jù)情況自動將棧上的block進行一次
copy操作,將block復(fù)制到堆上:
- block作為函數(shù)返回值時;
- block賦值給
__strong指針時;- block作為Cocoa API中方法名含有
usingBlock的方法參數(shù)時;- block作為GCD API的方法參數(shù)時。
(1) block作為函數(shù)返回值時:
typedef void(^MyBlock)(void);
MyBlock myblock() {
int age = 10;
return ^{
NSLog(@"-------- %d", age);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// block作為函數(shù)返回值
MyBlock block = myblock();
block();
// MRC打?。篲_NSStackBlock__ (訪問了auto變量)
// ARC打?。篲_NSMallocBlock__
NSLog(@"%@", [block class]);
}
return 0;
}
block訪問auto變量時,block的類型為__NSStackBlock__;
ARC將棧上的block進行一次copy操作,將block復(fù)制到堆上,并在適當?shù)牡胤竭M行release操作,所以ARC打印block為__NSMallocBlock__類型。
(2) 將block賦值給__strong指針時:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// block內(nèi)沒有訪問auto變量
MyBlock block = ^{
NSLog(@"block---------");
};
NSLog(@"%@", [block class]);
// block內(nèi)訪問了auto變量,但沒有賦值給__strong指針
int age = 10;
NSLog(@"%@", [^{
NSLog(@"block1--------- %d", age);
} class]);
// block賦值給__strong指針
MyBlock block2 = ^{
NSLog(@"block2--------- %d", age);
};
NSLog(@"%@", [block2 class]);
}
return 0;
}

由打印結(jié)果可知:
將block賦值給__strong指針時,RAC會自動進行一次copy操作。
(3) block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時:
例如:遍歷數(shù)組的block方法,將block作為參數(shù)
NSArray *array = @[];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
(4) block作為GCD API的方法參數(shù)時:
例如:GCD的一次性函數(shù)或延遲執(zhí)行的函數(shù)
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
MRC下block屬性的建議寫法:
@property (copy, nonatomic) void (^block)(void);ARC下block屬性的建議寫法:
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);