Blocks篇:4.Blocks的存储域(ARC下_NSConcreteBlock的陷阱)
在上一节中我们知道,在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陷阱
先看代码:
|
|
执行情况,我们可以直接得到个漂亮的“EXC_BAD_ACCESS”。
在图中已经看出,这种情况下,编译器并没有将捕获有变量的Block拷贝至堆中。故在准备执行时,Block对象已经被释放(第一个被转成__NSConcreteMallocBlock的原因是由于NSArray的init方法会自动保留对象,进而发生了Block的copy操作)。当执行完毕后,由于数组对象的释放,在对其内部元素依次释放时访问了野指针,导致崩溃。
所以,在集合中使用Block对象时,为了保证其安全性,我们可以有两种方式:
- 手动将Block复制到堆中:
|
|
由于Block是OC对象,故对其发送copy消息可以直接将其转换为__NSConcreteMallocBlock对象。
- 在初始化Block时,利用ARC的特性,对Block进行显式声明,以获取“strong”修饰的Block,自动生成NSConcreteMallocBlock对象:
|
|
注意:例外情况
在系统带有Block参数的API中(如GCD或是Animation相关等等),无需手动对Block进行复制(其内部实现已经包含了复制操作)。
2.Blocks的保留操作
2.1 Blocks的保留解析
我们知道,在生成__strong修饰的Block对象时,其实隐含的对生成的对象进行了retain操作。此操作实际为:
|
|
在NSObject.mm中,我们找到了此方法的实现:
|
|
因此,对Block进行retain其实也就是进行了copy操作,进而在堆上生成了Block。
2.2 Blocks的copy操作
现在,我们知道了对栈中的Block进行复制或保留操作,会在堆内存上生成对应的Block对象。但对于其他两者呢?
copy对应Block | 效果 |
---|---|
_NSConcreteGlobalBlock | 无作用 |
_NSConcreteMallocBlock | 引用计数 + 1 |
对于堆内存中的Block对象,其实是遵循了引用计数的内存管理方式。因此,在使用Block对象时,也要注意引用循环问题。