一直以来我都认为在 ARC 下,给 Cocoa 框架的集合类,如 NSArray,添加 Block 类型的元素时,Block 是会被编译器自动执行 copy
操作的。而且一直以来的实践也验证了这一事实。但今天在测试如下一段代码时出现了问题。
问题描述
先看下出问题的测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
如上所示,在获取数组中第二个 Block 元素时,crash 了,原因是 EXC_BAD_ACCESS
,即访问了已被释放的无效内存。很奇怪,调试打印 arr
,输出如下:
1 2 3 4 |
|
居然一个在堆上,一个在栈上,这。。。有些挑战三观了。
测试验证
确实很奇怪,那不妨来试试给数组填充元素的其他方式吧:
1 2 3 4 5 6 7 8 9 |
|
以及:
1 2 |
|
这两种情况下调试打印 arr
,输出如下:
1 2 3 4 |
|
可以看到都没问题,作为 addObject:
参数添加进来的两个 Block 元素,都被编译器自动执行了 copy
操作,这样 Block 的类型就变成了 __NSMallocBlock
,被拷贝到了堆上。好险,三观稍微又正了点儿。但文章开头的问题究竟是什么原因呢?
探寻原因
比较上边的测试代码和出问题的代码,同样都是 ARC 的测试环境,为什么问题代码中数组的两个 Block 元素,第一个在堆上,第二个在栈上呢?联想到像测试代码中这样,将 Block 拷贝到堆上的操作是编译器在编译时完成的,那问题会不会出在初始化方法上呢?然后点开出问题的那个 API:
1
|
|
果然!这里的参数是可变个数的,而且只有第一个参数显式的声明为 id
类型。这下就能解释问题代码中,为什么第一个 Block 元素在堆上而第二个却在栈上:因为只有第一个参数显式的声明为 id
类型,所以编译器在编译阶段只能意识到需要对第一个作为参数传进来的 Block 进行 copy
处理。为了验证这一猜测,下面显式得把后边的 Block 传参强制转换为 id
类型,让编译器看到
它:
1 2 3 |
|
代码顺利运行通过,没有 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 的理解。