为什么Block超出变量作用域还可以继续存在?

Posted Haley_Wong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么Block超出变量作用域还可以继续存在?相关的知识,希望对你有一定的参考价值。

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方法会有什么变化呢?

以上是关于为什么Block超出变量作用域还可以继续存在?的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript中作用域和作用域链解析

perl中my变量和local变量之间的区别

__block修饰符(四)

C基础作用域和内存管理

变量超出范围不起作用

iOS底层探索之Block——Block的探索和源码分析