原文地址:http://matrixzk.github.io/blog/20150518/store_blocks_in_NSArray/
一直以来我都认为在 ARC 下,给 Cocoa 框架的集合类,如 NSArray,添加 Block 类型的元素时,Block 是会被编译器自动执行 copy
操作的。而且一直以来的实践也验证了这一事实。但今天在测试如下一段代码时出现了问题。
问题描述
先看下出问题的测试代码:
id getBlockArray() {
int val = 12;
NSArray *arr = [[NSArray alloc] initWithObjects:^{NSLog(@"block1 val = %d", val);},
^{NSLog(@"block2 val = %d", val);},
nil];
return arr;
}
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
id blkArray = [obj getBlockArray];
MyBlock block1 = blkArray[0];
MyBlock block2 = blkArray[1]; // EXC_BAD_ACCESS, crash !!!
blcok1();
blcok2();
return 0;
}
如上所示,在获取数组中第二个 Block 元素时,crash 了,原因是 EXC_BAD_ACCESS
,即访问了已被释放的无效内存。很奇怪,调试打印 arr
,输出如下:
<__NSArrayM 0x100300500>(
<__NSMallocBlock__: 0x100300410>,
<__NSStackBlock__: 0x7fff5fbff750>
)
居然一个在堆上,一个在栈上,这。。。有些挑战三观了。
测试验证
确实很奇怪,那不妨来试试给数组填充元素的其他方式吧:
- (id)getBlockArray {
int val = 10;
NSMutableArray *arr = [[NSMutableArray alloc] init];
[arr addObject:^{NSLog(@"block1 val = %d", val);}];
[arr addObject:^{NSLog(@"block2 val = %d", val);}];
return arr;
}
以及:
int val = 10;
NSArray *arr = @[^{NSLog(@"block1 val = %d", val);}, ^{NSLog(@"block2 val = %d", val);}];
这两种情况下调试打印 arr
,输出如下:
<__NSArrayM 0x100100250>(
<__NSMallocBlock__: 0x100200550>,
<__NSMallocBlock__: 0x1003007e0>
)
可以看到都没问题,作为 addObject:
参数添加进来的两个 Block 元素,都被编译器自动执行了 copy
操作,这样 Block 的类型就变成了 __NSMallocBlock
,被拷贝到了堆上。好险,三观稍微又正了点儿。但文章开头的问题究竟是什么原因呢?
探寻原因
比较上边的测试代码和出问题的代码,同样都是 ARC 的测试环境,为什么问题代码中数组的两个 Block 元素,第一个在堆上,第二个在栈上呢?联想到像测试代码中这样,将 Block 拷贝到堆上的操作是编译器在编译时完成的,那问题会不会出在初始化方法上呢?然后点开出问题的那个 API:
- (instancetype)initWithObjects:(id)firstObj, ... NS_REQUIRES_NIL_TERMINATION;
果然!这里的参数是可变个数的,而且只有第一个参数显式的声明为 id
类型。这下就能解释问题代码中,为什么第一个 Block 元素在堆上而第二个却在栈上:因为只有第一个参数显式的声明为 id
类型,所以编译器在编译阶段只能意识到需要对第一个作为参数传进来的 Block 进行 copy
处理。为了验证这一猜测,下面显式得把后边的 Block 传参强制转换为 id
类型,让编译器看到
它:
NSArray *arr = [[NSArray alloc] initWithObjects:^{NSLog(@"block1 val = %d", val);},
(id)^{NSLog(@"block2 val = %d", val);},
nil];
代码顺利运行通过,没有 crash,猜测得到了验证。这真算是一个坑点儿。在 stackoverflow 上看到了一个对类似问题的讨论,可以参考下:Storing Blocks in an Array。
扩展
另外需要注意的一点是,在 MRC 下,向方法或函数的参数中传递 Block 时,除了以下两种情况,都需要手动 copy
一下:
- Cocoa 框架中的方法名含有 usingBlock 的方法。例如 NSArray 的
enumerateObjectsUsingBlock
实例方法。 - GCD 的 API。例如,
dispatch_async
函数。
在 ARC 下,除了上述两种情况外,在如下两种情况,编译器也帮我们自动做了 copy
操作:
- Block 作为函数或方法的返回值返回时。(此场景和 ARC 下普通的对象作为函数或方法返回值返回时的场景一致)
- 将 Block 赋值给附有
__strong
修饰符的变量时。(ARC 下的局部变量和成员变量默认都是__strong
的,只是作用域不同)
这里有一个有趣的小测试Objective-C Blocks Quiz,可以测下自己对 Block 的理解。