上次在群里闲聊,然后有人抛出了一道关于Block的题目,通过讨论跟自己代码断点调试,也算是对Block的相关知识的复习,也在理解上领悟多了一点。
本着好记性不如烂笔头的态度,所以记下来。
题目大概如下
下面此代码能运行通过吗?为什么?
@property (nonatomic, weak) void (^block)();
- (void)viewDidLoad {
[super viewDidLoad];
void (^ __weak blockA)() = ^{
NSLog(@"Hello");
};
_block = blockA;
}
- (IBAction)buttonClick:(id)sender {
_block();
}
答案是可以运行通过的。
在回答为什么?
这个问题前,我们先来温故下有关这道题的Block知识点。
Block的存储域
我们都知道,Block是“带有自动变量值得匿名函数”,而Block的实质就是OC对象
。那么既然是OC对象
,那么结构体内必然有一个ISA
指针指向它所属的类,而这个类正是表明了Block的存储域,属于哪种类型Block。
Block的类
- _NSConcreteStackBlock
- _NSConcreteGlobalBlock
- _NSConcreteMallocBlock
首先,我们能够注意到_NSConcreteStackBlock
类的名称中含有“栈”(stack)一词,即该类的对象Block设置在栈上
同样地,_NSConcreteGlobalBlock
类对象如其名“全局”(global)所示,与全局变量一样,设置在程序的数据区域(.data区)中
_NSConcreteMallocBlock
类对象则设置在由malloc函数分配的内存块(即堆)中
有时候图片比文字更好理解:
那怎么知道Block所属的类是哪个?
这里有个点,就是Block如果不引用外部变量, 那么编译器就会把它当做_NSConcreteGlobalBlock
处理,也就是相当于全局变量了。
而如果Block引用了外部变量,那么它会是_NSConcreteStackBlock
或者_NSConcreteMallocBlock
类型,而一般情况下,都会是_NSConcreteMallocBlock
。
NSLog(@"%@", [_block class]);
就可以在控制台输出看到block所属的类了
回到上面的题目,就可以解释为什么按钮点击,_block
可以正常执行,因为该Block并没有引用外部变量,所以它是Global
类型的
那如果,该block引用了外部变量呢?答案会是什么?
我们把代码稍作修改
NSString *name = @"Word";
void (^ __weak blockA)() = ^{
NSLog(@"Hello %@", name);
};
再次运行,点击button,这时候程序异常,全局断点跳到了_block()
这一行,很经典的EXC_BAD_ACCESS
,也就是说,我们访问了block这个已经销毁的内存。
并且通过控制台输出,我们也能看到该block的类是__NSStackBlock__
,也就是存储在栈
上的,超出其作用域,内存自然被回收了。
如果想让block超过其作用域也能使用,就得把它从栈
复制到堆
上。
Block从栈复制到堆
那么什么时候栈上的Block会复制到堆呢?
- 调用Block的copy实力方法时
- Block作为函数返回值返回时
- 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
- 在方法名中含有usingBlock的Cocoa框架方法或Grand Central Dispatch 的API中传递Block时
所以,如果想让上述修改后的代码能正常运行,就可以修改如下:
_block = [blockA copy]; // 手动调用copy方法
这样block就被复制到堆上,进而在button的事件回调方法中内存也是存在的。
后话
其实,这只是作为一道题才会将一个block对象赋值给一个_weak
修饰的变量,这本身就不规范,实际开发中,我们并不会这么写,就像你也不会这样去定义一个NSObject对象
id __weak object = [[NSObject alloc] init];
NSLog(@"%@", object);
这样这个对象在初始化之后,就立即释放了,object是为nil
的
不过你可能会注意到,如果这么写,object是不为nil
id obj = [[NSObject alloc] init];
id __weak object = obj;
NSLog(@"%@", object);
与被赋值时相比,在使用附有_weak修饰符变量的情形下,增加了对objc_loadWeakRetained函数和objc_autorelease函数的调用,这两函数分别是做以下事情:
- objc_loadWeakRetained函数取出附有__weak修饰符变量锁引用的对象并retain
- objc_autorelease函数将对象注册到autoreleasePool中
因为附有__weak修饰符变量锁引用的对象被注册到autoreleasePool中,所以在@autoreleasePool块结束前都可以放心使用的。关于这个更详细的知识,可以去看iOS多线程那本书
测试中发现的现象
本着好奇心的态度,如果block是作为数组的元素,会是怎样?
@property (nonatomic, copy) NSArray *blocks;
- (void)viewDidLoad {
[super viewDidLoad];
NSString *string = @"boy";
_blocks = @[^{NSLog(@"HelloWorld, %@", string);}, ^{NSLog(@"HelloWorld, %@", string);}, ^{NSLog(@"HelloWorld");}];
}
- (IBAction)buttonClick:(id)sender {
for (void (^blockA)() in _blocks) {
NSLog(@"%@", [blockA class]);
blockA();
}
}
通过看控制台的输出,跟我们预计的结果一样,一个是malloc block,一个是global block
2017-05-01 17:46:51.780 BlockTest[2302:437716] __NSMallocBlock__
2017-05-01 17:46:51.783 BlockTest[2302:437716] HelloWorld, boy
2017-05-01 17:46:51.783 BlockTest[2302:437716] __NSMallocBlock__
2017-05-01 17:46:51.783 BlockTest[2302:437716] HelloWorld, boy
2017-05-01 17:46:51.785 BlockTest[2302:437716] __NSGlobalBlock__
2017-05-01 17:46:51.785 BlockTest[2302:437716] HelloWorld
但是,如果我们使用NSArray
的- (instancetype)initWithObjects:
API来代替语法糖的初始化
_blocks = [[NSArray alloc] initWithObjects:^{NSLog(@"HelloWorld, %@", string);}, ^{NSLog(@"HelloWorld, %@", string);}, ^{NSLog(@"HelloWorld");}, nil];
再次运行,点击button崩溃了,同样是抛出EXC_BAD_ACCESS
异常,可以看到数组里的元素是这样的。
对于这个使用语法糖跟API的不同结果,很是困惑不解,对于之前一直觉得
使用语法糖内部就是调用了这个init的API
的想法,可能要打个问号了。