1. 关于 Block 的几道题
1. void exampleA() {
char a = 'A';
^{
printf("%c\n", a);
}()
}
The example ()
A. always works.
B. only works with ARC.
C. only works without ARC.
D. never works.
2. void exampleB_addBlockToArray(NSMutableArray *array) {
char b = 'B';
[array addObject:^{
printf("%c\n", b);
}];
}
void exampleB() {
NSMutableArray *array = [NSMutableArray array];
exampleB_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block()
}
The example ()
A. always works.
B. only works with ARC.
C. only works without ARC.
D. never works.
3. void exampleC_addBlockToArray(NSMutableArray *array) {
[array addObject:^{
printf("C\n");
}];
}
void exampleC() {
NSMutableArray *array = [NSMutableArray array];
exampleC_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}
The example ()
A. always works.
B. only works with ARC.
C. only works without ARC.
D. never works.
4. typedef void (^dBlock)();
dBlock exampleD_getBlock() {
char d = 'D';
return ^{
printf("%c\n", d);
};
}
void exampleD() {
exampleD_getBlock()();
}
The example ()
A. always works.
B. only works with ARC.
C. only works without ARC.
D. never works.
5. typedef void (^eBlock)();
eBlock exampleE_getBlock() {
char e = 'E';
void (^block)() = ^{
printf("%c\n", e);
};
return block;
}
void exampleE() {
eBlock block = exampleE_getBlock();
block;
}
The example ()
A. always works.
B. only works with ARC.
C. only works without ARC.
D. never works.
结果分别为:A、B、A、B、B
解释:
- 第一题中,Block 访问外部变量 a,之后在栈上生成了一个同名的只读变量 a。需要注意的是,两个 a 的生命周期是相同的,同样都存在于栈上。由于调用函数 exampleA() 中调用了 Block,故 ARC 与非 ARC 都是可以运行的。
- 第二题中,如果不是在 ARC 模式下调用,则 Block 为
__NSStackBlock
类型 ,当在 exampleB() 中调用 Block 的时候,Block
是不可用的。在 ARC 模式下,Block 会自动 copy 到堆上面(也是自动释放的),是__NSMallocBlock
类型的。 - 第三题中,Block 没有访问任何的外部变量,则 Block 为
__NSGlobalBlock
类型的。它既不在堆上也不在栈上,而是在全局区中。 - 第四题中,与第二题类似,Block 访问了栈上的变量 d,但是在 exampleD() 函数中调用了 Block。在非 ARC 模式下,会报错:
error: returning block that lives on the local stack
。在 ARC 模式下,Block 会自动 copy 到堆上并且自动
release,其类型为__NSMallocBlock
。 - 第五题中,与第四题其实是一样的,只不过将 Block 返回给了一个局部变量,之后将这个局部变量返回。这样在非 ARC 模式下编译器不会再报错,但是还是会出现问题。ARC 模式下,Block 会自动 copy 到堆上,并且自动 release,类型为
__NSMallocBlock
。
2. Block 的分类
-
__NSGlobalBlock 类型(全局 Block)
总结:对于没有引用外部变量的 Block,无论是在 ARC 还是在非 ARC 下,类型都是__NSGlobalBlock
。这种类型的 Block 可以理解为一种全局的 Block,不需要考虑作用域的问题。同时,对它进行 copy 或者 retain 操作也都是无效的。 -
__NSStackBlock 类型(栈 Block)
总结:对于引用了外部局部变量(注意不是局部静态变量)的 Block,在 MRC 下如果没有对它进行 copy 操作,它的作用域只会在定义它的函数栈内(类型为__NSStackBlock
)。在 ARC 下,由于对象指针默认为__strong
修饰,故直接赋值的话 Block 会被 copy 到堆上。如果对象指针用__weak
修饰,则 Block 不会被拷贝到堆上面。
- __NSMallocBlock 类型(堆 Block)
总结:在 MRC 下,对 Block 进行 copy 操作后,Block 会被拷贝到堆上。在 ARC
下,由于对象指针默认由__strong
修饰,则程序默认会将 Block 拷贝到堆上(如何使用__weak
则程序不会拷贝 Block 到堆上)。
3. __block 关键字
- 默认情况
总结:由图中可以得知,Block 中的 a 与外部变量 a 它们的地址并不相同,但是值是相同的。实际上 Block 中的 a 是外部变量 a 的一个拷贝,且为常量。
- 使用 __block 关键字
总结:由图中可知,在使用了__block
关键字后,Block 中的 a 与外部变量 a 的地址相同。这种情况下 Block 中使用的就是货真价实的外部变量 a,而且可以对
a 进行赋值操作。
- copy 的情况
总结:由图可知,局部变量 a 在 Block 进行 copy 前和 copy 后的地址不同了。实际上对 Block 对象进行 copy 后,Block 中引用的变量都会被复制到堆上。而被标记为__block
的变量,实际被移动到了堆上。为什么要将 a 移动到堆上面呢?归根结底是因为 a 被声明了__block
。copy 之后,Block 中的 a 将被放置到堆上面,但是程序需要保持 Block 中的 a 与外部的 a 的一致性 (生命周期与作用域),故外部的 a 也就移动到了堆上面。
ps: 哪里有 copy 呢?
4. Block 的循环引用
Block 对外部引用的对象都会进行持有,直到 Block 执行完。如果此时 Block 持有的对象正好持有 Block,则会发生内存泄漏。
- 常见的 Block 循环引用
解释:学生类中定义了一个 Block 属性 work,此时学生类对象 s 持有 work 属性。在 work 的 Block 中,又访问了外部对象 s,则 work 持有对象 s,这就形成了一个环。
-
观察者模式引起的循环引用
解释:在通知中心的这个方法中,Block 中引用了 self 或者 self 的成员变量(无论是通过点方法还是直接访问实例变量),Block 都会持有当前对象。如果在对象的 dealloc 方法中将通知移除,则会形成循环引用。通知中心会一直持有该对象,直到解除 Observer 的注册。 -
NSTimer 引起的循环引用
解释:由图中可知,self 对象对 NSTimer 对象有强引用,而且又作为 NSTimer 对象的 target。而 NSTimer 对象会一直持有 target 对象,直到 NSTimer 对象不再有效 (invalidate
方法) ,这样就形成了一个循环引用。代码又在 self 对象的 dealloc 方法中对 NSTimer 进行销毁,而 self 对象的 dealloc 方法是在 self 对象将要释放的时候调用的,所以 self 对象和 NSTimer 对象永远都无法释放。
- NSURLSession 对象引起的循环引用
解释:NSURLSession 对象会对 delegate 保持强引用,直到程序释放或者调用了NSURLSession 对象的finishTasksAndInvalidate
方法或者invalidateAndCancel
方法。与上面的例子相似,在 dealloc 中调用 invalidate 相关方法,是无法解决循环引用的情况的,在使用的时候要多加注意。
5. Block 的底层结构
Block 的底层结构大体是由结构体以及额外的函数来构成,具体可以使用 clang 的命令编译 .m 文件来查看:clang -rewirte-objc xxx.m
。网上有很多分析的文章,这里就不再一一的分析了。
6. 解决循环引用
-
通过将Block中访问的对象设置为weak。
或者更加安全的方式:
-
使用完 Block 的时候及时将 Block 置为空
解释:循环引用的发生一般都是由于 Block 持有了对象,而对象又持有了 Block 。这样我们可以在使用完 Block 之后,立刻释放对象对 Block 的持有,将 Block 置空就可以打破引用环。
- 根据情况将环的任一强引用置空即可
在实际的开发中,有可能存在多个节点的复杂的环,除了可以将 Block 置空外,还可以打破环中的任一强引用,就可以解决掉循环引用的问题。
6. 练习
下面这四种情况中,哪个会存在内存泄漏的情况?
- (void)test1
{
self.student = [Student new];
self.student.work = ^{
self.name = @"Hello";
};
self.student.work = nil;
}
- (void)test2
{
self.student = [Student new];
self.student.work = ^{
self.name = @"Hello";
};
self.student = nil;
}
- (void)test3
{
Student *student = [Student new];
student.work = ^{
student.name = @"小明";
};
student.work = nil;
}
- (void)test4
{
Student *student = [Student new];
student.work = ^{
student.name = @"小明";
};
student = nil;
}
7. 总结
- ARC 下 Block 会从栈上自动拷贝到堆上,原因是由于
__strong
的原因。 - Block 由于会持有外部引用的变量,容易引发循环引用的问题。解决的办法是把环打破,或者根本不让环生成。
- iOS 开发中使用
NSTimer
、NSURLSession
、NSNotificationCenter
的时候一定要注意。 - 需要注意,非 ARC下
__block
可以解决循环引用的问题。在 ARC 下不可以,需要使用__weak
来解决。
8. 参考链接
- Block技巧与底层解析
- 谈Objective-C block的实现
- Objective-C中的Block