Blocks篇:4.Blocks的存储域
在上一节中我们知道,在Block捕获不同种类的变量时,生成的Block对象的类型(isa指针)分为三种:
- _NSConcreteStackBlock
- _NSConcreteGlobalBlock
- _NSConcreteMallocBlock
此三种类型的Block对象分别存储在栈区、全局(数据区)和堆区
我们知道,由于Block对象的函数体定义在Block实例化的生命周期外部,故其执行时早已不在原作用域内。况且,由于在函数中,定义的Block对象也是局部变量,超出作用域也会被自动回收。所以,要保证Block超出原作用域仍然可以存在的方式,就是将其转化为全局Block,或者复制到堆内存中,这样才可以保证其内存可控并正确执行Block的函数。在ARC环境下,LLVM在编译期已经可以在绝大多数情况下正确处理这种情况。通过测试,编码时定义的局部Block变量(即_NSConcreteStackBlock对象),在运行时可以得到如下结果:
捕获变量情况 | 运行期生成的Block对象 |
---|---|
无 | _NSConcreteGlobalBlock |
全局或静态变量 | _NSConcreteGlobalBlock |
普通局部变量 | _NSConcreteMallocBlock |
但是,发现在一种情况下,ARC不会自动处理,需要我们对Block对象进行手动转换。
1.ARC下的Blocks陷阱
先看代码:
// main.m
typedef void(^VoidBlock)(void);
/** 返回包含Block对象的集合的函数 */
NSArray *getBlocksArray () {
int myVal = 2;
// 内部的Block对象均为__NSConcreteStackBlock对象
return [[NSArray alloc] initWithObjects:
^{NSLog(@"block1~%d", myVal);},
^{NSLog(@"block2~%d", myVal);},
nil
];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 获取该数组
NSArray *blocksArray = getBlocksArray();
// 取出Block对象
VoidBlock voidBlock = blocksArray[0];
// 执行
voidBlock();
}
return 0;
}
执行情况,我们可以直接得到个漂亮的“EXC_BAD_ACCESS”。
Block在集合中的坑.jpg在图中已经看出,这种情况下,编译器并没有将捕获有变量的Block拷贝至堆中。故在准备执行时,Block对象已经被释放(第一个被转成__NSConcreteMallocBlock的原因是由于NSArray的init方法会自动保留对象,进而发生了Block的copy操作)。当执行完毕后,由于数组对象的释放,在对其内部元素依次释放时访问了野指针,导致崩溃。
所以,在集合中使用Block对象时,为了保证其安全性,我们可以有两种方式:
- 手动将Block复制到堆中:
NSArray *getBlocksArray () {
int myVal = 2;
return [[NSArray alloc] initWithObjects:
[^{NSLog(@"block1~%d", myVal);} copy],
[^{NSLog(@"block2~%d", myVal);} copy],
nil
];
}
由于Block是OC对象,故对其发送copy消息可以直接将其转换为__NSConcreteMallocBlock对象。
- 在初始化Block时,利用ARC的特性,对Block进行显式声明,以获取“__strong”修饰的Block,自动生成__NSConcreteMallocBlock对象:
NSArray *getBlocksArray () {
int myVal = 2;
// 生成了强引用Block变量,自动分配到了堆内存中
VoidBlock block1 = ^{NSLog(@"block1~%d", myVal);};
VoidBlock block2 = ^{NSLog(@"block2~%d", myVal);};
return [[NSArray alloc] initWithObjects:
block1,
block2,
nil
];
}
注意:例外情况
在系统带有Block参数的API中(如GCD或是Animation相关等等),无需手动对Block进行复制(其内部实现已经包含了复制操作)。
2.Blocks的保留操作
2.1 Blocks的保留解析
我们知道,在生成__strong修饰的Block对象时,其实隐含的对生成的对象进行了retain操作。此操作实际为:
VoidBlock blockObj = ...;
// 对Block进行保留操作
objc_retainBlock(blockObj);
...
在NSObject.mm中,我们找到了此方法的实现:
// NSObject.mm
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
因此,对Block进行retain其实也就是进行了copy操作,进而在堆上生成了Block。
2.2 Blocks的copy操作
现在,我们知道了对栈中的Block进行复制或保留操作,会在堆内存上生成对应的Block对象。但对于其他两者呢?
copy对应Block | 效果 |
---|---|
_NSConcreteGlobalBlock | 无作用 |
_NSConcreteMallocBlock | 引用计数 + 1 |
对于堆内存中的Block对象,其实是遵循了引用计数的内存管理方式。因此,在使用Block对象时,也要注意引用循环问题。
网友评论