1. block的本质
我们通过一个简单的demo,解析一下block的底层原理.
定义一个简单的block并调用:
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
^(){
NSLog(@"Hello world, I'm block!");
}();
}
return 0;
}
将OC代码转换成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相关代码
^(){
NSLog(@"Hello world, I'm block!");
}();
装换成的C++代码就是:
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();
block的调用实际上就是__main_block_impl_0
这个结构体,结构体实现如下:
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
结构体内部有一个与结构体同名的__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)
函数,这是C++结构体的写法,该函数为结构体的构造函数,相当于OC类中的- (instancetype)init;
方法。__main_block_impl_0
函数携带三个参数,最后一个参数为可选的,默认值为0。再看结构体__main_block_impl_0
,发现其第一个成员imp也是个结构体,结构体类型为__block_impl
。
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
通过结构体__block_impl
实现代码中的isa指针,显而易见这是个对象,因此可以准确地说block的本质是一个OC对象。结构体的第二个成员仍然是个__main_block_desc_0
类型的结构体.
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
该结构体两个成员一个是系统的保留值reserved = 0,另一个Block_size则代表了该block的大小。
接下来回到block的调用函数__main_block_impl_0
,((void (*)())&__main_block_impl_0((void*)__main_block_func_0,&__main_block_desc_0_DATA))();
该函数就是结构体__main_block_impl_0
的构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)
,它有两个必传的参数,一个是函数指针fp ,一个是结构体指针desc,关于结构体指针所指向的结构体就是上面分析到的__main_block_desc_0
,那么第一个参数函数指针fp到底是什么?在这个demo的C++实现代码中,fp指向的函数为__main_block_func_0
,__main_block_func_0
的函数实现代码如下:
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体内打印了一个字符串,与这个__main_block_func_0
函数内的代码完全一致。研究发现__main_block_func_0
这个函数的作用就是将block体内的代码封装成一个函数,也就是说block体内的所有OC代码被封装成__main_block_func_0
这个函数。与我们OC中的代码NSLog(@"Hello world, I'm block!");
相对应的就是NSLog((NSString*)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);
.
通过上面的分析,可以肯定的是block本质上也是一个OC对象,它内部也有一个isa指针,block还是一个封装了函数的调用的OC对象。
2. block的变量捕获(capture)
一. 局部变量之auto变量
什么是auto变量?局部变量有哪几种?
所谓的auto变量就是非静态的局部变量,离开作用于就会销毁。例如下面这个函数:
- (void)example{
int a = 5; //等价于auto int a = 5;
NSString *name = @"Block"; //等价于 auto NSString *name = @"Block";
static int b = 10; //这个b就不是auto变量
}
常识小结:通常情况下我们定义的局部非static变量都是auto变量,系统会默认在前面加上auto关键字的;但是静态局部变量就不会有auto前缀,加了也会由于报错而编译不通过。
为了保证block内部能够正常访问外部的变量,block有个变量捕获机制,这个变量捕获机制之后block变成什么样子?:
//demoA
#import
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内部访问局部变量a的值,后面在调用block之前修改了a的值,但是打印出来的a的结果仍然为修改之前的值,将上边的代码转换成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结构体,此时的block所对应的结构体__main_block_impl_0
里面多了一个成员int a
,并且结构体的构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a)
多出了一个参数_a
,(知识点:后面的: a(_a)
为C++的语法,意为将参数_a
赋值给成员a
)。
在实现block的时候,对应的C++代码为Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,&__main_block_desc_0_DATA, a));
可见,系统将a
作为函数__main_block_impl_0
的参数传递进去,所以block所对应的结构体中int a;
这个成员所对应的值a = 10;
后面我们修改了a
的值为20,并使用block();
调用block 打印a
的值,这个时候调用了函数__main_block_func_0(struct __main_block_impl_0 *__cself)
,实现如下:
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);
}
其内部访问变量a
的方式为:int a = __cself->a;__cself
为block所对应的结构体对象,所以这个a
也就是之前结构体__main_block_impl_0
中保存的成员变量a
的值,即为10,而不是后面修改的20。针对这个问题,我的看法是block在调用的时候,其实此时main()
函数中的a
变量相对于block来说是个外部的变量,因为block对应的结构体内部有自己的变量a
,外面怎么修改不会影响到block结构体内部成员a
的值。
二. 局部变量之static变量
根据demoA,我们在demoB中中block内部增加访问静态的局部变量static int b以及修改a、b变量的值后,调用block打印的结果:
//demoB
#import
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;
}
发现局部静态变量b修改之后,block内部打印的结果也变了!
局部变量a的访问过程demoA已经分析过了,接下来仍旧通过C++代码研究局部静态变量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++代码发现,局部自动变量a
与静态变量b
的捕获方式不同,block结构体中,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
变量的指针,调用block的时候,__main_block_func_0
中获取b
也是通过block的结构体__main_block_impl_0
访问内部成员变量b
,与结构体外部变量b
指向的是同一块内存地址,所以只要有地方修改b
,结构体内部也会跟随变化,这样就解释了为啥“同样修改了局部auto变量与局部static变量,block访问的结果不同”。
总而言之:在block内部访问的auto变量为值传递,局部静态变量为引用传递(也就是传递变量的指针)。
三. 全局变量
\\demoB
#import
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内部访问全局变量age_
,其变化同静态局部变量一样。同样转换成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结构体内部没有定义age_
变量,block内部访问age_
变量的时候,传入的也是全局的age_
,因此在任何地方改变这个全局变量,block访问的时候都是这个全局变量的最新值。
通过demoA\B\C,可以肯定对于局部auto变量、static变量、全局变量,block的变量捕获情况如下:
分析了block对自动变量,static变量与全局变量的捕获方式的不同,我认为合理的解释是:自动变量,内存可能会销毁,将来执行block的时候,访问变量的内存,可能会因为不存在引发坏内存访问。
静态局部变量:static变量内存一直会保存在内存中,所以可以取它的最新值,也就是通过指针去取。
3. block的类型
block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型.
__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )
我们通过关键字可以知道这三种类型block分别存放在内存的全局区、栈区、堆区,在内存中对应的区域图示如下:
程序区域存放的就是我们写的代码,比如一个Person类里面的代码。
数据区也就全局区,存放着程序中使用到的全局变量。
堆存放的就是我们新建的对象。如[[Person alloc] init]出来的,这部分内存需要我们手动释放。
栈区存放的就是自动变量,一般在函数调用之后,这些自动变量所占用内存也就被系统回收了。
补充的知识
堆是动态分配内存的,是程序员管理的,自己申请内存,自己管理内存.
栈是系统会自动分配内存,自己释放内存.
由于在ARC环境下,编译器为我们做了很多额外的工作,比如将栈区的block copy到堆区,我们在ARC下也就不容易捕获到block初始状态的位置。所以暂时将开发环境切换至MRC下:
在MRC下,定义两个block,一个访问auto变量,一个不访问auto变量,最后对访问auto变量的block调用copy方法,依次查看三种情况下block所对应的类型如下:
#import
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在栈区,不访问auto变量的block在全局区。对栈区的block调用copy方法,block居然移到了堆区!后面我们对全局区的block调用copy,发现全局区域的block仍旧在全局区。
block类型 | 环境 |
---|---|
NSGlobalBlock | 没有访问auto变量 |
NSStackBlock | 访问了auto变量 |
NSMallocBlock | NSStackBlock调用了copy |
stackBlock为什么要copy到堆上,因为我们想把block存储的东西保存下来.栈上的block会在函数结束的时候释放,block保存的东西不确定,我们一般都是将block保存下来,在恰当的时候调用.
4. block的copy
在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,有以下情况:
- block作为函数返回值的时.
- 将block赋值给_strong指针时.
- block作为Cocoa API中方法名含有usingBlock的方法参数时.(例如:[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { }];)
- block作为GCD API的方法参数时
- 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环境下,在某个函数内定义了一个block变量,并在block中访问了局部变量,但是并没有立即调用该block。后面等到调用该函数的时候,再调用block,看下面的demo:
调用block后,block内部访问的局部变量打印的结果很糟糕,程序倒是没奔溃,但是结果不如人所愿。
出现这种情况的原因很好理解:由于这个block访问了auto变量,因此是一个NSStackBlock
类型的block,该block对应的结构体分配在栈内存上,等到test()
函数调用完毕,栈内存会被回收,所以block被调用的时候,访问block结构体内部的变量a
,a
所对应的内存区域随时可能被系统回收,其内存上的数据也是不确定的。
这种情况该如何保证我们调用block的时候,还能正常访问局部变量呢?正如前面列出的,调用copy
方法将block从栈区copy
到堆区,事情就解决了。【当然,换成ARC环境,我们通常在声明block属性的时候,使用copy
或strong
关键词修饰,系统也会自动帮我们将block从栈区拷贝到堆区。也就无需我们动手调用block的copy
方法了。但是系统底层还是帮我们对block做了copy
操作。
"copy"这个操作在ARC下是没有必要的。由于我们的block赋值给了void(^block)(void)
,这个变量默认是__strong
修饰的,满足编译器会根据情况自动将栈上的block复制到堆上的条件2,即"将block赋值给__strong
指针时"。
想了解更多iOS学习知识请联系:QQ(814299221)