Block中的存储域
1.Block的类型
通过前文说明可知,Block会转换为Block的结构体类型的自动变量(例如__main_block_impl_0
类型的自动变量),__block
修饰的变量会转换为__block
变量的结构体类型的自动变量(例如__Block_byref_count_0
类型的自动变量)。所谓结构体类型的自动变量,也就是栈上生成的该结构体类型的实例。
Block
自动变量,即栈上的Block结构体实例。
__block
自动变量,即栈上的__block
变量的结构体实例。
通过之前的说明可知Block也是Objective-C对象(因为其内部也有一个isa指针)。将Block当做对象来看时,该Block的类为_NSConcreteStackBlock
。该类是在其他框架中声明,与之类型的类有:_NSConcreteGlobalBlock
、_NSConcreteStackBlock
、_NSConcreteMallocBlock
。
_NSConcreteStackBlock
很明显,该类型的Block设置在栈上。_NSConcreteGlobalBlock
也正如其名中的Global,该类型的Block设置在全局变量区。_NSConcreteMallocBlock
类型的Block则设置在由malloc函数分配的内存块中,也就是堆区。
2._NSConcreteGlobalBlock类型的Block创建
当我们在记述全局变量的地方使用Block语法时,创建的Block就是_NSConcreteGlobalBlock
类型的。
例如:
#import <Foundation/Foundation.h>
void (^blk)(void) = ^ {printf("__block 测试");};
int main(int argc, const char * argv[]) {
blk();
return 0;
}
该源码,经过Clang 转换后的代码中,block用结构体中的isa赋值是这样的:
impl.isa = &_NSConcreteGlobalBlock;
该Block的类为_NSConcreteGlobalBlock
。即该Block的结构体实例设置在程序的全局变量区,因为使用全局变量的地方不能使用自动变量,因此就不存在对自动变量的截获,在_NSConcreteGlobalBlock
类型的block中只能使用全局变量。
总结如下:
在记述全局变量的地方使用Block语法时,创建的Block就是_NSConcreteGlobalBlock
类型对象。Block设置在全局变量区。除此,之外的Block语法生成的Block为_NSConcreteStackBlock
类型对象,并且被设置在栈上。
_NSConcreteMallocBlock类型Block
配置在全局变量上的Block,在任何地方都可以通过指针安全地使用。但设置在栈上的Block,如果其所属的变量作用域结束,该Block就会被废弃。而__block变量也被设置在栈上,如果其所属的变量作用域结束,则该__block变量也会被废弃。
怎么解决这个问题呢?
Blocks提供了将Block和__block变量从栈上复制到堆上的方法来解决这个问题。将配置在栈上的Block复制到堆上,这样即使Block语法所在的变量作用域结束,堆上的Block还可以继续存在。
复制到堆上的Block将_NSConcreteMallocBlock
类对象写入Block用结构体实例的成员变量isa。
impl.isa = &_NSConcreteMallocBlock;
而__block变量用结构体成员变量__forwarding 可以实现无论__block变量配置在栈上还是在堆上时都能够正确地访问__block变量。
有时在__block变量配置在堆上的状态下,也可以访问栈上的__block变量。在这种情况下,只要栈上的结构体实例的成员变量__forwarding 指向堆上的结构体实例,那么不管是从栈上的__block变量还是从栈上的__block变量都能够正确访问。
当ARC有效时,大多数情形下编译器会恰当地进行判断,自动生成将Block从栈上复制到堆上的代码。
例如,返回block的函数:
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count){ return rate * count; };
}
在ARC下的编译器转换后如下:
blk_t func(int rate) {
blk_t tmp = &__func_block_impl_0(__func_objc_func_0, &__func_block_desc_0_DATA, rate);
tmp = objc_retainBlock(tmp);
return objc_autoreleaseReturnValue(tmp);
}
然后,在objc4-750
中的NSObject.mm
中找到objc_retainBlock
的实现:
//
// The -fobjc-arc flag causes the compiler to issue calls to objc_{retain/release/autorelease/retain_block}
//
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
在arc 下,该函数内部调用的就是_Block_copy
函数。 所以转换后:
blk_t func(int rate) {
/**
通过Block语法生成的栈上的Block结构体实例,
将该实例赋值给Block类型的变量tmp。
*/
blk_t tmp = &__func_block_impl_0(__func_objc_func_0, &__func_block_desc_0_DATA, rate);
/**
通过_Block_copy函数,将栈上的Block复制到堆上。
复制后,将堆上的地址作为指针赋值给变量tmp。
*/
tmp = _Block_copy(tmp);
/**
将堆上的Block作为Objective-C对象,
注册到autoreleasepool中,然后返回该对象。
*/
return objc_autoreleaseReturnValue(tmp);
}
在ARC下,将Block作为函数返回值返回时,编译器会自动生成复制到堆上的代码。除此之外,还有不少情况编译器会适当地进行判断,然后将Block从栈区复制到堆区。除了编译器会自动判断的情况外,我们如果想要将Block从栈区复制到堆上,可以使用copy 方法。
因为Block也是Objective-C对象,所以也可以使用copy方法,而使用copy方法时,block也会从栈区复制到堆区。
那么有哪些情况需要我们手动的调用copy方法,才能将Block从堆区复制到栈区呢?
- 向方法或函数的参数中传递Block,且方法或函数内也没有适当地复制传递过来的block参数时。
有一些情况不需要主动调用copy函数复制:
- Cocoa框架中的方法并且方法名中含有usingBlock时。
- Grand Central Dispatch 的API。
下面看一下需要手动调用copy函数,防止栈上block被释放的例子。
- (NSArray *)getBlockArray
{
int val = 10;
return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0:%d", val);} copy],[^{NSLog(@"blk0:%d", val);} copy], nil];
}
// 然后在某函数中,比如viewDidLoad函数中
NSArray *tmpArray = [self getBlockArray];
typedef void (^blk_t)(void);
blk_t blk = (blk_t)[tmpArray objectAtIndex:0];
blk();
这里在调用blk()时,程序就会Crash。这是由于在getBlockArray函数执行结束时,栈上的Block被废弃的缘故。由于将Block从栈上复制到堆上是挺消耗CPU的。所以,我们需要根据实际场景,在一些超出Block作用域还要使用block的情况下,手动的调用copy函数,来将Block从栈上复制到堆上。 如果,在栈上依然能正确访问到Block,却将Block复制到堆上,其实是浪费CPU资源的。
上面的源代码,可以这样修改:
- (NSArray *)getBlockArray
{
int val = 10;
return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0:%d", val);} copy],[^{NSLog(@"blk0:%d", val);} copy], nil];
}
对于Block语法可以直接调用copy方法,当然对于Block类型变量也是可以调用copy方法的。
对于不同存储域的Block实例,调用copy方法会有什么变化呢?