开年第一餐,__weak 关键字用于防止 block 造成循环引用,关于它的用法,以及误区,一起来品尝吧。
关于 __weak
__weak 关键字是伴随着 ARC 内存管理机制而来的一个变量修饰符,用于防止循环引用。 使用过 block 的朋友可能都会看到过类似这样的建议:
__weak ViewController* weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[weakSelf doSome];
});
如上所示, 为什么 block 里面要使用 __weak 形式的引用呢? 大家常见的说法基本是这样 —— 因为 block 本身会对在它内部使用的所有引用进行 strong 类型的捕获。 如果我们直接在 block 里面直接引用 self, 它就会对 self 进行 strong 类型的引用。 而与此同时, self 也会对 block 进行 strong 引用。 这样就会引起内存循环引用,这两个对象所占用的内存都无法被释放掉。
实例验证
这个说法是否完全正确呢? 我花时间实践了一下,只能说它在理论上是对的,但实践中也要根据具体情况而定。咱们来看一个例子, 我们定义一个 Reporter 类:
@interface Reporter : UIView
- (void)block: (void (^)(void)) doBlock;
- (void) foo;
@property (strong) void (^doBlock)(void);
@end
@implementation Reporter
- (instancetype)init {
self = [super init];
if(self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification) name:@"Notification" object:nil];
}
return self;
}
- (void) handleNotification {
NSLog(@"notification reveiced");
}
- (void) foo {
//Just for demo.
}
- (void)block: (void (^)(void)) doBlock {
self.doBlock = doBlock;
self.doBlock();
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
NSLog(@"dealloc");
}
@end
这个类不算复杂,在 init 初始化的时候注册了一个通知,只要这个对象还在内存中存在,就会处理这个通知。 我们用这个来测试对象当前是否被销毁。
还有另外两个方法, foo 用作示例, block 方法接受一个 block 类型的参数,并且会赋值给它的一个 strong 类型的属性,然后执行这个传递进来的 block。
dealloc 方法取消了注册通知,并且输出一行消息。
实验环境就创建完成了,现在我们在合适的地方使用 Reporter 这个类:
Reporter *_reporter = [[Reporter alloc] init];
[[NSNotificationCenter defaultCenter] postNotificationName:@"Notification" object:nil];
[_reporter block:^{
}];
_reporter = nil;
dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5);
dispatch_after(when, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"Notification" object:nil];
});
我们这里先初始化一个 Reporter 对象, 然后紧接着发送一个 Notification 消息, 这时候 Reporter 应该可以正确的处理这个消息,并打印控制台输出。
然后,我们调用 [_reporter block] 方法,传入的 block 中保留空白。 紧接着把 _reporter 赋值为 nil。
这时 _reporter 应该已经被释放了。 我们来验证一下, 使用 dispatch_after, 在 5秒钟之后,再次发送 Notification 通知消息。果然这次没有得到控制台输出, 因为处理消息的 Reporter 对象已经不存在了。
一切都在逻辑之中,但如果这时我们对上面的代码稍加改动,情况就会不一样:
[_reporter block:^{
[_reporter foo];
}];
这里我们唯一做的改动就是在传入的 block 中加了一行代码 [_reporter foo]。 这时再次运行我们的程序,你会发现,虽然我们在第一次发送通知后,将 _reporter 设置为 nil 了。 但 5秒钟之后,依然输出了处理通知的消息。 也就是说 Reporter 对象这次没有被销毁。
造成这个现象的原因就是循环引用。 还记得我们定义 Reporter 的时候吗:
@property (strong) void (^doBlock)(void);
这个 doBlock 的属性我们定义为 strong 类型的强引用。 而我们刚刚做的改动,在 block 内部加入 [_reporter foo] 的调用,相当于在 block 中又反过来对 Reporter 也进行了强引用。
这样他们两个的实例都不能被正常销毁,所以出现了我们第二次看到的现象。
如何解决
那么如何解决这个问题呢,有两种方法,第一种就是我们最开始提到的 __weak 引用:
__weak Reporter* weakReporter = _reporter;
[_reporter block:^{
[weakReporter foo];
}];
这样再运行程序,就恢复到正常结果了。 __weak 这个标记会告诉 block 不要对它引用的这个实例进行 strong 强引用。 两个强引用只要断掉其中一个,实例就可以被正常销毁了。
就像我们提到的,两个强引用只要断掉一个就可以,也就是说除了使用 __weak 断掉 block 中的强引用,我们还可以断掉另一端:
@interface Reporter
@property (weak) void (^doBlock)(void);
@end
这次我们把 Reporter 的 doBlock 改成 weak 类型的。 这样我们在调用出还可以这样写:
[_reporter block:^{
[_reporter foo];
}];
这次运行结果也正常了。 虽然 block 会对 _reporter 进行强引用, 但 _reporter 对 block 是弱引用。
用在适合的场景
好了,上面说了这么多后,这里到了真正想和大家讨论的地方了。 就是所有使用 block 引用外部变量的地方都必须使用 __weak 引用吗?
答案显而易见,肯定不是,我们刚刚的例子就是个证明。这取决于是否构成循环引用。如果使用引用 block 本身的类不是强引用,我们其实就不需要在调用的时候使用 __weak 了。
比如 GCD 和 View Animation 的 block:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
});
[UIView animateWithDuration:0.2 animations:^{
}];
在我实际使用它们的时候,我发现不使用 __weak 并不会造成循环引用。 但在另外一些情况下,就需要注意这个问题, 比如 ASIHTTP 中:
ASIHTTPRequest *req = [[ASIHTTPRequest alloc] initWithURL:[NSURL URLWithString:@""]];
[req setCompletionBlock:^{
[req setTag:1];
}];
ASIHTTP 的 setCompletionBlock 方法会对传入的 block 进行强引用。 所以 block 内部在引用 reuqest 对象的时候,就需要加上 __weak 修饰符了。
通常在这种情况下,编译器会给出警告:
按照这个提示,加上 __weak 引用即可。
结语
以上就是我对 weak 修饰符和它和 block 的使用问题跟大家进行的讨论内容了。 虽然很多文章中会告诉我们在任何用到 block 的地方都要使用 weak。 但在我的实践中,发现只要对那些可能造成循环引用的地方,才有必要使用 weak。 并不是所有的 block 都会造成循环引用。 当然,这都是基于我目前对它的认知总结出来的内容,有可能还有不足,也欢迎大家在留言中展开和补充。
更多精彩内容可关注微信公众号:
swift-cafe