文章目录
一、Block是什么?
int main(int argc, const char * argv[]) {
int a = 0;
void (^aBlock)(void) = ^{
NSLog(@"%d", a);
};
aBlock();
}
用clang转为cpp
- Block是带有自动变量(局部变量)的匿名函数,在OC中Block本质上就是一个对象(__main_block_impl_0结构体类比objc_object结构体)。
- Block截获自动变量是Block语法表达式所使用的自动变量值被保存到了Block的结构体实例中。
1.1 __block结构
上面的Block中的变量a是不允许进行修改的,原因是__main_block_func_0中把a进行了值拷贝,此时修改的只是临时变量a,而不是__cself->a,会引发歧义,所以不允许更改。那__block为什么可以更改变量
呢?把上面的int a = 0;用__block修饰,然后再来看clang后的代码。
可以看到加上__block后a变为了一个__Block_byref_a_0
类型的变量,对它的操作也变成了a->__forwarding->a
。具体可看下面1.2.2小节。
1.2 存储域
建议直接去看《Obj-C高级编程》书中相关内容。
1.2.1 Block存储域
将Block当作OC对象来看时,可看到它的isa指向_NSConcreteStackBlock,常用到的有3种:
类 | 设置对象的存储域 | 区分 |
---|---|---|
_NSConcreteStackBlock | 栈 | 截获自动变量 并且在该变量作用域内 |
_NSConcreteGlobalBlock | 程序的数据区域(.data区) | 记述全局变量的地方有Block语法时(在定义全局变量的地方定义Block);Block语法的表达式中不使用 应截获的自动变量 时 |
_NSConcreteMallocBlock | 堆 | 当_NSConcreteStackBlock超出变量作用域ARC下 大多数情况下编译器 进行适当判断后 调用_Block_copy拷贝到堆上 |
在ARC下编译器大多数情况会适当地进行判断然后自动从栈复制到堆,那编译器在什么情况下不能判断需要手动复制呢?
- 向方法或函数的参数中传递Block时
但是如果在方法或函数中适当地复制了传递过来的参数,那么就不必在调用该方法或函数前手动复制了。以下方法或函数不用手动复制
。- Cocoa框架的方法且方法名中含有usingBlock等时
- GCD的API
下面来具体看段代码来理解
typedef void (^blk_t)(void);
NSArray *getBlockArray() {
int val = 10;
//ARC不会自动复制,需手动复制
return [[NSArray alloc] initWithObjects:^{
NSLog(@"blk0: %d", val);}, ^{
NSLog(@"blk1: %d", val);}, nil];
// return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0: %d", val);} copy], [^{NSLog(@"blk1: %d", val);} copy],nil];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
void (^globalBlock)(void) = ^{
};
//__NSGlobalBlock__
NSLog(@"GlobalBlock is %@", globalBlock);
__block int a = 10;
void (^stackBlock)(void) = ^void {
a++; };
//MRC __NSStackBlock__
NSLog(@"StackBlock is %@", stackBlock);
//ARC __NSMallocBlock__
NSLog(@"MallocBlock is %@", stackBlock);
NSArray *array = getBlockArray();
blk_t blk = (blk_t)[array objectAtIndex:0];
blk(); //如果没有手动复制,崩溃。因getBlockArray()执行完后,栈上的Block被废弃。
}
return 0;
}
1.2.2 __block变量存储域
使用__block变量的Block从栈复制到堆上时,__block变量也会受到影响。
__block变量的配置存储域 | Block从栈复制到堆时的影响 |
---|---|
栈 | 从栈复制到堆并被Block持有 |
堆 | 被Block持有 |
在一个Block中使用__block变量
在多个Block中使用__block变量
堆上Block被废弃,它所使用的__block变量也就被释放。
- 那么__block变量用结构体成员变量__forwarding的原因是什么?
通过Block的复制,__block变量也从栈复制到堆,此时可同时访问栈上的__block变量和堆上的__block变量。源代码如下:
__block int val = 0;
void (^blk)(void) = [^{
++val;} copy];
++val;
blk();
NSLog(@"%d", val);
用clang转以后
可以看出都是++(val.__forwarding->val),但是栈上的__block变量用结构体实例在___block从栈复制到堆上时,会将成员变量__forwording的值替换为复制目标上的block变量用结构体实例的地址。
通过该功能,无论是在Block语法中、Block语法外使用__block变量,还是__block变量配置在栈上或堆上,都可以顺利地访问同一个__block变量。
1.3 捕获对象
NSNumber *val = @(0);
void (^blk)(void) = ^{
NSLog(@"%@", val);};
blk();
在OC中,C语言结构体不能含有附有__strong修饰符的变量,因为编译器不知道应何时时行C语言结构体的初始化和废弃操作,不能很好地管理内存。但是OC的运行时库能准确把握从栈复制到堆以及堆上的Block被废弃的时机,因此Block结构体中即使含有附有__strong修饰符或__weak修饰符的变量,也可以恰当地进行初始化和废弃。为此需要使用在__main_block_desc_0结构体中增加的成员变量copy和dispose,以及作为指针赋值给该成员变量的__main_block_copy_0函数和__main_block_dispose_0函数。
调用copy函数和dispose函数的时机:
函数 | 调用时机 |
---|---|
copy函数 | 栈上的Block复制到堆时 |
dispose函数 | 堆上的Block被废弃时 |
那么什么时候栈上的Block会复制到堆呢?
- 调用Block的copy实例方法时
- Block作为函数返回值返回时
- 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
- 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时
那么问题来了,在使用__block修饰的int a时也产生了copy、dispose方法,怎么与对象区分的呢?
基本类型 | 对象 | __block修饰基本类型 | __block修饰对象 | |
---|---|---|---|---|
Block结构体 | 不加入copy、dispose成员变量 | 加入copy、dispose成员变量,flag为BLOCK_FIELD_IS_OBJECT | 加入copy、dispose成员变量,flag为BLOCK_FIELD_IS_BYREF | 加入copy、dispose成员变量,flag为BLOCK_FIELD_IS_BYREF |
__block变量结构体 | – | – | 不加入copy、dispose成员变量 | 加入copy、dispose成员变量 |
1.4 Block循环引用
typedef void (^block_t)(void);
@interface TestA : NSObject
@property(nonatomic, copy) block_t aBlock;
@property(nonatomic, copy) NSString