回到前文中演示的__main_block_impl_0的代码
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
这里讲解一下block结构体的__main_block_impl_0的构造函数中isa指针的值类型及其含义。
首先isa指向实例对象,正好表明Block也跟一般的OC对象类似,拥有isa指针,共有三种block类型:
- NSConcreteStackBlock:设置在栈上
- NSConcreteGlobalBlock:设置在全局范围上,即与全局变量一样,设置在程序的数据区域
- NSConcreteMallocBlock:设置在堆上
下面分别举例说明这三种情况在什么时候发生,以及对变量访问的限制区别
NSConcreteGlobalBlock
OC源码:
#import <Foundation/Foundation.h>
void (^bBlock)(void)=^{NSLog(@"hahaha");};
int main(int argc, char * argv[]) {
...
C源码:
struct __bBlock_block_impl_0 {
struct __block_impl impl;
struct __bBlock_block_desc_0* Desc;
__bBlock_block_impl_0(void *fp, struct __bBlock_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteGlobalBlock;//全局block,无法截获自动变量
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
当将block声明在main函数之前即函数全局变量时,impl.isa的值即为NSConcreteGlobalBlock,但是由于相当于全局变量,所以无法截获自动变量,这种block不会在执行时发生任何改变,所以将其存储于数据区域中。
所以可以推断出除了在函数的全局变量处声明block能将block类型设置为NSConcreteGlobalBlock外,在函数中声明block时,只要block的执行函数中不包含任何自动变量,即其内容不会在执行时发生改变时,就可以得到NSConcreteGlobalBlock类型的block。作者试验过,的确如此。
NSConcreteStackBlock
非某些情况下,在函数中声明的block均为NSConcreteStackBlock类型,正如前面所列举的所有例子均是生成的NSConcreteStackBlock类型。
OC源码:
__block int i = 1;
void (^aBlock)() = ^{
NSLog(@"%d",i);
};//aBlock已经被复制到了堆上
i=2;
aBlock();
在上述源码中,按之前所理解的,这里应该生成的是NSConcreteStackBlock类型的block,但是,在aBlock();处打上断点,Xcode截取到的aBlock类型竟然为NSConcreteMallocBlock,如下图所示:
这是因为在ARC的环境下,声明block时默认为__strong,所以编译器就自动的将block复制到了堆上,所以在运行时得到的block为NSConcreteMallocBlock,将ARC关闭,则可得到想要的NSConcreteStackBlock。
附上ARC关闭开起的设置:
由于NSConcreteStackBlock是生成在栈上的,当其所属的变量作用域结束时,该block就会被废弃,同时配置在栈上的block变量也会被废弃,所以Block提供了将Block与__block变量复制到堆上的方法,来防止因变量作用域结束而废弃的情况,而被复制后的Block的impl.isa值即为NSConcreteMallocBlock了。
附上代码举例:
(为了验证NSConcreteStackBlock下列代码在MRC的环境下运行)
typedef void (^blk_t)(void);
@implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
blk_t blk = [self testBlk];
blk();
}
- (blk_t)testBlk{
int j = 0;
blk_t tttt = ^{NSLog(@"tttt%d",j);};
return tttt;
}
这段代码中先定义了一个blk_t类型的block,然后在私有方法中构建一个block,将这个block作为返回值。然后在viewDidLoad 中调用testBlk获取到其返回的blk。程序运行结果:
由此可得到当testBlk函数运行结束时,在其函数作用域上的NSConcreteStackBlock类型的block tttt也被释放了,所以在后面运行blk时会出现EXC_BAD_ACCESS的崩溃。
NSConcreteMallocBlock
正如前面所列举的例子所示,在ARC有效的环境下,编译器会自动将block复制到堆上(大多数情况下)。
在此情况下编译器无法自动判断,需手动调用copy函数,将block复制到堆上:
- 向方法或函数的参数中传递Block时
但下列两种传递参数的情况除外,编译器会自动复制block
- Cocoa框架的方法且方法名中含有usingBlock等时
- GCD的API
附上代码举例:(将编译环境修改回ARC)
typedef void (^blk_t)(void);
@implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
blk_t blk = [self testBlk];
blk();
}
- (blk_t)testBlk{
int j = 0;
blk_t tttt = ^{NSLog(@"tttt%d",j);};
return tttt;
}//输出tttt0
同样的一段代码,在ARC环境下能正常运行,因为在ARC下声明tttt时,已经将其复制到堆上了,所以当testBlk运行结束时,由于blk持有了testBlk的返回结果,所以引用计数加一,tttt就没有被释放掉,所以可以正常访问。
下列代码为书中的例子,作者在运行时遇到了跟书中例子解释不同的地方,在此贴出试验代码与结果:
- (void)viewDidLoad {
[super viewDidLoad];
NSDictionary *tt = [self getDic];
blk_t blk = [tt objectForKey:@"2"];
blk();
}
- (NSDictionary* )getDic {
int i = 1;
NSDictionary *ttt = [[NSDictionary alloc] initWithObjectsAndKeys:^{NSLog(@"first %d",i);},@"1",^{NSLog(@"second %d",i);} ,@"2", nil];
return ttt;
}
上述代码中,为字典初始化了两个键值对,值均为block,但是根据书中所述,获取到的字典值应该均为NSConcreteStackBlock类型,但是经过重复试验,在ARC有效的环境下,无论是对NSArray还是NSDictionary通过initwithobject或initWithObjectsAndKeys的方式传入block时,第一个总是为NSConcreteMallocBlock,后面block的均为NSConcreteStackBlock如下图所示:
输出结果如下:
- 情况一:
blk_t blk = [tt objectForKey:@"2"];
blk();
访问的是NSConcreteStackBlock,程序崩溃在blk()处,与书中的描述相符,这是因为在NSDictionary *tt = [self getDic]执行结束时,栈上的Block被废弃,访问不存在的变量引起的崩溃。
- 情况二:
blk_t blk = [tt objectForKey:@"1"];
blk();
访问的是NSConcreteMallocBlock,程序在执行完blk();后,崩溃在main.m的return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
函数上,目前尚未找出原因,如果有人知晓原因的话欢迎留言探讨!
作者猜测可能是NSArray与NSDictionary通过initwithobject或initWithObjectsAndKeys的方式传入变量时,NSArray与NSDictionary会对首元素进行某种操作,使其不会存储在栈上,但是既然为NSArray与NSDictionary对象所持有,那为什么在访问后会崩溃呢?这是作者一直未想明白的地方。