开年第一餐,__weak 关键字用于防止 block 造成循环引用,关于它的用法,以及误区,一起来品尝吧。
__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 类:
@interfaceReporter:UIView - (void)block: (void (^)(void)) doBlock; - (void) foo; @property (strong) void (^doBlock)(void); @end @implementationReporter - (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 中的强引用,我们还可以断掉另一端:
@interfaceReporter @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,会有更多我的原创内容分享给你~