1. block的本質(zhì)
我們通過一個簡單的demo,解析一下block的底層原理.
定義一個簡單的block并調(diào)用:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
^(){
NSLog(@"Hello world, I'm block!");
}();
}
return 0;
}
將OC代碼轉(zhuǎn)換成C++代碼
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;
__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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);
}
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)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();
}
return 0;
}
//我們OC下的block相關(guān)代碼
^(){
NSLog(@"Hello world, I'm block!");
}();
裝換成的C++代碼就是:
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();
block的調(diào)用實際上就是__main_block_impl_0
這個結(jié)構(gòu)體,結(jié)構(gòu)體實現(xiàn)如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__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
結(jié)構(gòu)體內(nèi)部有一個與結(jié)構(gòu)體同名的__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)
函數(shù),這是C++結(jié)構(gòu)體的寫法,該函數(shù)為結(jié)構(gòu)體的構(gòu)造函數(shù),相當于OC類中的- (instancetype)init;
方法。__main_block_impl_0
函數(shù)攜帶三個參數(shù),最后一個參數(shù)為可選的,默認值為0。再看結(jié)構(gòu)體__main_block_impl_0
,發(fā)現(xiàn)其第一個成員imp也是個結(jié)構(gòu)體,結(jié)構(gòu)體類型為__block_impl
。
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
通過結(jié)構(gòu)體__block_impl
實現(xiàn)代碼中的isa指針,顯而易見這是個對象,因此可以準確地說block的本質(zhì)是一個OC對象。結(jié)構(gòu)體的第二個成員仍然是個__main_block_desc_0
類型的結(jié)構(gòu)體.
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
該結(jié)構(gòu)體兩個成員一個是系統(tǒng)的保留值reserved = 0,另一個Block_size則代表了該block的大小。
接下來回到block的調(diào)用函數(shù)__main_block_impl_0
,((void (*)())&__main_block_impl_0((void*)__main_block_func_0,&__main_block_desc_0_DATA))();
該函數(shù)就是結(jié)構(gòu)體__main_block_impl_0
的構(gòu)造函數(shù)__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)
,它有兩個必傳的參數(shù),一個是函數(shù)指針fp ,一個是結(jié)構(gòu)體指針desc,關(guān)于結(jié)構(gòu)體指針所指向的結(jié)構(gòu)體就是上面分析到的__main_block_desc_0
,那么第一個參數(shù)函數(shù)指針fp到底是什么?在這個demo的C++實現(xiàn)代碼中,fp指向的函數(shù)為__main_block_func_0
,__main_block_func_0
的函數(shù)實現(xiàn)代碼如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);
}
由于在OC代碼中,我們block體內(nèi)打印了一個字符串,與這個__main_block_func_0
函數(shù)內(nèi)的代碼完全一致。研究發(fā)現(xiàn)__main_block_func_0
這個函數(shù)的作用就是將block體內(nèi)的代碼封裝成一個函數(shù),也就是說block體內(nèi)的所有OC代碼被封裝成__main_block_func_0
這個函數(shù)。與我們OC中的代碼NSLog(@"Hello world, I'm block!");
相對應(yīng)的就是NSLog((NSString*)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);
.
通過上面的分析,可以肯定的是block本質(zhì)上也是一個OC對象,它內(nèi)部也有一個isa指針,block還是一個封裝了函數(shù)的調(diào)用的OC對象。
2. block的變量捕獲(capture)
一. 局部變量之a(chǎn)uto變量
什么是auto變量?局部變量有哪幾種?
所謂的auto變量就是非靜態(tài)的局部變量,離開作用于就會銷毀。例如下面這個函數(shù):
- (void)example{
int a = 5; //等價于auto int a = 5;
NSString *name = @"Block"; //等價于 auto NSString *name = @"Block";
static int b = 10; //這個b就不是auto變量
}
常識小結(jié):通常情況下我們定義的局部非static變量都是auto變量,系統(tǒng)會默認在前面加上auto關(guān)鍵字的;但是靜態(tài)局部變量就不會有auto前綴,加了也會由于報錯而編譯不通過。
為了保證block內(nèi)部能夠正常訪問外部的變量,block有個變量捕獲機制,這個變量捕獲機制之后block變成什么樣子?:
//demoA
#import <Foundation/Foundation.h>
typedef void(^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
Block block = ^(){
NSLog(@"你好世界!a = : %d ;",a);
};
a = 20;
block();
// 2018-08-28 21:34:33.276996+0800 Interview03-block[99340:9151961] 你好世界!a = : 10 ;
}
return 0;
}
上面demo,block內(nèi)部訪問局部變量a的值,后面在調(diào)用block之前修改了a的值,但是打印出來的a的結(jié)果仍然為修改之前的值,將上邊的代碼轉(zhuǎn)換成C++代碼:
typedef void(*Block)(void);
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_09a2a6_mi_0,a);
}
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)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
對比之前沒有訪問任何變量的block結(jié)構(gòu)體,此時的block所對應(yīng)的結(jié)構(gòu)體__main_block_impl_0
里面多了一個成員int a
,并且結(jié)構(gòu)體的構(gòu)造函數(shù)__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a)
多出了一個參數(shù)_a
,(知識點:后面的: a(_a)
為C++的語法,意為將參數(shù)_a
賦值給成員a
)。
在實現(xiàn)block的時候,對應(yīng)的C++代碼為Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,&__main_block_desc_0_DATA, a));
可見,系統(tǒng)將a
作為函數(shù)__main_block_impl_0
的參數(shù)傳遞進去,所以block所對應(yīng)的結(jié)構(gòu)體中int a;
這個成員所對應(yīng)的值a = 10;
后面我們修改了a
的值為20,并使用block();
調(diào)用block 打印a
的值,這個時候調(diào)用了函數(shù)__main_block_func_0(struct __main_block_impl_0 *__cself)
,實現(xiàn)如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_09a2a6_mi_0,a);
}
其內(nèi)部訪問變量a
的方式為:int a = __cself->a;__cself
為block所對應(yīng)的結(jié)構(gòu)體對象,所以這個a
也就是之前結(jié)構(gòu)體__main_block_impl_0
中保存的成員變量a
的值,即為10,而不是后面修改的20。針對這個問題,我的看法是block在調(diào)用的時候,其實此時main()
函數(shù)中的a
變量相對于block來說是個外部的變量,因為block對應(yīng)的結(jié)構(gòu)體內(nèi)部有自己的變量a
,外面怎么修改不會影響到block結(jié)構(gòu)體內(nèi)部成員a
的值。
二. 局部變量之static變量
根據(jù)demoA,我們在demoB中中block內(nèi)部增加訪問靜態(tài)的局部變量static int b以及修改a、b變量的值后,調(diào)用block打印的結(jié)果:
//demoB
#import <Foundation/Foundation.h>
typedef void(^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
auto NSString *name = @"Block";
int a = 10;
static int b = 10;
Block block = ^(){
NSLog(@"你好世界!a = : %d ;",a);
NSLog(@"你好世界!b = : %d ;",b);
};
a = 20;
b = 20;
block();
// 2018-08-28 23:16:53.244791+0800 Interview03-block[861:9731638] 你好世界!a = : 10 ;
// 2018-08-28 23:16:53.245153+0800 Interview03-block[861:9731638] 你好世界!b = : 20 ;
}
return 0;
}
發(fā)現(xiàn)局部靜態(tài)變量b修改之后,block內(nèi)部打印的結(jié)果也變了!
局部變量a的訪問過程demoA已經(jīng)分析過了,接下來仍舊通過C++代碼研究局部靜態(tài)變量b的捕獲過程:
typedef void(*Block)(void);
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *b;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
int *b = __cself->b; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_0b7d13_mi_0,a);
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_0b7d13_mi_1,(*b));
}
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)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
static int b = 10;
Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b));
a = 20;
b = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
通過C++代碼發(fā)現(xiàn),局部自動變量a
與靜態(tài)變量b
的捕獲方式不同,block結(jié)構(gòu)體中,a
為int
變量,b
為int *
變量,也就是指針。在定義block的時候,Block block = ((void (*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, a, &b))
,傳遞的也是b
變量的指針,調(diào)用block的時候,__main_block_func_0
中獲取b
也是通過block的結(jié)構(gòu)體__main_block_impl_0
訪問內(nèi)部成員變量b
,與結(jié)構(gòu)體外部變量b
指向的是同一塊內(nèi)存地址,所以只要有地方修改b
,結(jié)構(gòu)體內(nèi)部也會跟隨變化,這樣就解釋了為啥“同樣修改了局部auto變量與局部static變量,block訪問的結(jié)果不同”。
總而言之:在block內(nèi)部訪問的auto變量為值傳遞,局部靜態(tài)變量為引用傳遞(也就是傳遞變量的指針)。
三. 全局變量
\\demoB
#import <Foundation/Foundation.h>
typedef void(^Block)(void);
int age_ = 25;
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
static int b = 10;
Block block = ^(){
NSLog(@"你好世界!a = : %d ;",a);
NSLog(@"你好世界!b = : %d ;",b);
NSLog(@"你好世界!age_ = : %d ;",age_);
};
a = 20;
b = 20;
age_ = 26;
block();
// 2018-08-29 00:54:13.318712+0800 Interview03-block[2155:10283110] 你好世界!a = : 10 ;
// 2018-08-29 00:54:13.319099+0800 Interview03-block[2155:10283110] 你好世界!b = : 20 ;
// 2018-08-29 00:54:13.319130+0800 Interview03-block[2155:10283110] 你好世界!age_ = : 26 ;
}
return 0;
}
block內(nèi)部訪問全局變量age_
,其變化同靜態(tài)局部變量一樣。同樣轉(zhuǎn)換成C++代碼分析:
typedef void(*Block)(void);
int age_ = 25;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *b;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
int *b = __cself->b; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_0,a);
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_1,(*b));
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_2,age_);
}
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)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
static int b = 10;
Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b));
a = 20;
b = 20;
age_ = 26;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
較demoA、demoB不同的是,block結(jié)構(gòu)體內(nèi)部沒有定義age_
變量,block內(nèi)部訪問age_
變量的時候,傳入的也是全局的age_
,因此在任何地方改變這個全局變量,block訪問的時候都是這個全局變量的最新值。
通過demoA\B\C,可以肯定對于局部auto變量、static變量、全局變量,block的變量捕獲情況如下:
分析了block對自動變量,static變量與全局變量的捕獲方式的不同,我認為合理的解釋是:自動變量,內(nèi)存可能會銷毀,將來執(zhí)行block的時候,訪問變量的內(nèi)存,可能會因為不存在引發(fā)壞內(nèi)存訪問。
靜態(tài)局部變量:static變量內(nèi)存一直會保存在內(nèi)存中,所以可以取它的最新值,也就是通過指針去取。
3. block的類型
block有3種類型,可以通過調(diào)用class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型.
__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )
我們通過關(guān)鍵字可以知道這三種類型block分別存放在內(nèi)存的全局區(qū)、棧區(qū)、堆區(qū),在內(nèi)存中對應(yīng)的區(qū)域圖示如下:
程序區(qū)域存放的就是我們寫的代碼,比如一個Person類里面的代碼。
數(shù)據(jù)區(qū)也就全局區(qū),存放著程序中使用到的全局變量。
堆存放的就是我們新建的對象。如[[Person alloc] init]出來的,這部分內(nèi)存需要我們手動釋放。
棧區(qū)存放的就是自動變量,一般在函數(shù)調(diào)用之后,這些自動變量所占用內(nèi)存也就被系統(tǒng)回收了。
補充的知識
堆是動態(tài)分配內(nèi)存的,是程序員管理的,自己申請內(nèi)存,自己管理內(nèi)存.
棧是系統(tǒng)會自動分配內(nèi)存,自己釋放內(nèi)存.
由于在ARC環(huán)境下,編譯器為我們做了很多額外的工作,比如將棧區(qū)的block copy到堆區(qū),我們在ARC下也就不容易捕獲到block初始狀態(tài)的位置。所以暫時將開發(fā)環(huán)境切換至MRC下:
在MRC下,定義兩個block,一個訪問auto變量,一個不訪問auto變量,最后對訪問auto變量的block調(diào)用copy方法,依次查看三種情況下block所對應(yīng)的類型如下:
#import <Foundation/Foundation.h>
typedef void(^Block)(void);
int age_ = 25;
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
static int b = 10;
Block block = ^(){
NSLog(@"你好世界!a = : %d ;",a);
};
Block block1 = ^(){
NSLog(@"你好世界");
};
NSLog(@"%@ %@ %@",[block class],[block1 class],[[block copy] class]);
// __NSStackBlock__ __NSGlobalBlock__ __NSMallocBlock__
}
return 0;
}
訪問了auto變量的block在棧區(qū),不訪問auto變量的block在全局區(qū)。對棧區(qū)的block調(diào)用copy方法,block居然移到了堆區(qū)!后面我們對全局區(qū)的block調(diào)用copy,發(fā)現(xiàn)全局區(qū)域的block仍舊在全局區(qū)。
block類型 | 環(huán)境 |
---|---|
NSGlobalBlock | 沒有訪問auto變量 |
NSStackBlock | 訪問了auto變量 |
NSMallocBlock | NSStackBlock調(diào)用了copy |
stackBlock為什么要copy到堆上,因為我們想把block存儲的東西保存下來.棧上的block會在函數(shù)結(jié)束的時候釋放,block保存的東西不確定,我們一般都是將block保存下來,在恰當?shù)臅r候調(diào)用.
4. block的copy
在ARC環(huán)境下,編譯器會根據(jù)情況自動將棧上的block復制到堆上,有以下情況:
- block作為函數(shù)返回值的時.
- 將block賦值給_strong指針時.
- block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時.(例如:[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { }];)
- block作為GCD API的方法參數(shù)時
- MRC下block屬性的建議寫法
@property (copy, nonatomic) void (^block)(void);- ARC下block屬性的建議寫法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
那么問題來了,為什么要對block進行copy操作?
假如在MRC環(huán)境下,在某個函數(shù)內(nèi)定義了一個block變量,并在block中訪問了局部變量,但是并沒有立即調(diào)用該block。后面等到調(diào)用該函數(shù)的時候,再調(diào)用block,看下面的demo:
調(diào)用block后,block內(nèi)部訪問的局部變量打印的結(jié)果很糟糕,程序倒是沒奔潰,但是結(jié)果不如人所愿。
出現(xiàn)這種情況的原因很好理解:由于這個block訪問了auto變量,因此是一個NSStackBlock
類型的block,該block對應(yīng)的結(jié)構(gòu)體分配在棧內(nèi)存上,等到test()
函數(shù)調(diào)用完畢,棧內(nèi)存會被回收,所以block被調(diào)用的時候,訪問block結(jié)構(gòu)體內(nèi)部的變量a
,a
所對應(yīng)的內(nèi)存區(qū)域隨時可能被系統(tǒng)回收,其內(nèi)存上的數(shù)據(jù)也是不確定的。
這種情況該如何保證我們調(diào)用block的時候,還能正常訪問局部變量呢?正如前面列出的,調(diào)用copy
方法將block從棧區(qū)copy
到堆區(qū),事情就解決了。【當然,換成ARC環(huán)境,我們通常在聲明block屬性的時候,使用copy
或strong
關(guān)鍵詞修飾,系統(tǒng)也會自動幫我們將block從棧區(qū)拷貝到堆區(qū)。也就無需我們動手調(diào)用block的copy
方法了。但是系統(tǒng)底層還是幫我們對block做了copy
操作。
"copy"這個操作在ARC下是沒有必要的。由于我們的block賦值給了void(^block)(void)
,這個變量默認是__strong
修飾的,滿足編譯器會根據(jù)情況自動將棧上的block復制到堆上的條件2,即"將block賦值給__strong
指針時"。
想了解更多iOS學習知識請聯(lián)系:QQ(814299221)