Block存储域
</br>
1、全局块出现的2种场景
impl.isa = &_NSConcreteGlobalBlock;
(1) 记述全局变量的地方有Block语法时
void (^blk)(void) = ^{ printf("Global Block\n"); };
int main() {
...
}
(2)Block语法的表达式中不使用截获的自动变量时
for (int rate = 0; rate < 10; ++rate) {
(void) (^blk)(void) = ^{int count){ return count; };
}
2、栈块
impl.isa = &_NSConcreteStackBlock;
除了上述全局块的场景外,Block语法生成的Block对象是_NSConcreteStackBlock,因此设置在栈上。
配置在全局变量上的Block,从变量作用域外也可以通过指针安全地使用。但是设置在栈上的Block,如果其所属的变量作用域结束,该Block就被废弃,如同一般的自动变量。当然,Block中的__block变量也同时被废弃。
3、堆块
impl.isa = &_NSConcreteMallocBlock;
为了解决栈块在其变量作用域结束之后被废弃(释放)的问题,我们需要把Block复制到堆中,延长其生命周期。(说起延长生命周期,让我想起在一个非alloc/new/copy/mutableCopy方法中创建对象后,把它添加到autoreleasepool的做法。)
开启ARC时,大多数情况下编译器会恰当地进行判断是否有需要将Block从栈复制到堆,如果有,自动生成将Block从栈上复制到堆上的代码。Block的复制操作执行的是copy实例方法。
看一个返回值为Block类型的函数
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count) { return rate * count; };
}
分析可知:上面的函数返回的Block是配置在栈上的,所以返回函数调用方时,Block变量作用域就结束了,Block会被废弃。但在ARC有效,这种情况编译器会自动完成复制。
�转换为C++代码看其是如何做到的:
blk_t func(int rate) {
blk_t tmp = &__func_block_impl_0 (__func_block_func_0, &__func_block_desc_0_DATA, rate);
tmp = objc_retainBlock(tmp);
return objc_autoreleaseReturnValue(tmp);
}
第一条语句,将通过Block语法生成的配置在栈上的Block,赋值给变量tmp。
第二条语句,通过运行时库可知,objc_retainBlock函数实际上是_Block_copy函数。_Block_copy函数负责将栈上的Block赋值到堆上,复制后将堆上的地址作为指针赋值给变量tmp。
第三条语句,将堆上的Block作为Objective-C对象,注册到autoreleasepool中,然后返回该对象。
编译器不能进行判断的情况,需要编程人员调用copy方法手动复制:
- 向方法或函数的参数中传递Block时
如果在方法或函数中适当地复制了传递过来的参数,就不必在调用该方法或函数前手动复制。以下方法或函数不用手动复制:
- Cocoa框架的方法且方法名中含有usingBlock,如NSArray类的enumerateObjectsUsingBlock实例方法。
- Grand Central Dispatch的API,如dispatch_async函数。
!注意:将Block从栈上复制到堆上相当消耗CPU,所以当Block设置在栈上也能够使用时,就不要复制了,因为此时的复制只是在浪费CPU资源。
Block的复制操作执行的是copy实例方法。不同类型的Block使用copy方法的效果如下表
根据表得知,Block在堆中copy会造成引用计数增加,这与其他Objective-C对象是一样的。虽然Block在栈中也是以对象的身份存在,但是栈块没有引用计数,因为不需要,我们都知道栈区的内存由编译器自动分配释放。
至此,总结栈上的Block什么时候会复制到堆上:
- 调用Block的copy实例方法时
- Block作为函数返回值返回时
- 将Block赋值给附有__strong修饰符id类型的类或Block类型变量时
- 在方法名中含有usingBlock的Cocoa框架方法或Grand Central Dispatch的API中传递Block时
其中,第一种情况需要程序猿手动调用,后面三种情况由编译器自动完成(开启ARC)。