循环引用
原因:
互相持有 self ---> block ---> self
循环本质
A B两个对象,A持有B 则B的retainCount +1,
A B释放条件是各自retainCount = 0的时候,调用自身dealloc,开始释放自动释放池内的所有变量,进行释放。
A 给B 发送release信息才会让B的retainCount -1 ,释放前提是A的count=0,执行A的dealloc。
但是如果此时B 也持有A,那么A的retainCount +1,则A就不会执行dealloc,也不会对B进行release了。
如何解决Block循环
解决思路
第一个办法是「事前避免」,我们在会产生循环引用的地方使用 weak 弱引用,以避免产生循环引用。
第二个办法是「事后补救」,我们明确知道会存在循环引用,但是我们在合理的位置主动断开环中的一个引用,使得对象得以回收。
第三个办法是「只使用不持有」,我们在block参数中传入使用的对象,这样我们block不持有这个对象,但是我们还可以继续使用这个对象。
- 方法一: weak strong dance。 技术方案
- 方法二: __block 合理设nil 。 技术方案
- 方法三: 传对象进入block 。 技术方案
下面我们先模拟一个循环引用案例
#import "SecViewController.h"
typedef void(^myBlock)(void);
@interface SecViewController ()
@property (nonatomic, copy) myBlock myblock;
@property (nonatomic, copy) NSString *name;
@end
@implementation SecViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.myblock = ^{
self.name = @"小明";
};
self.myblock();
}
- (void)dealloc
{
NSLog(@"执行%@释放",self.class);
}
@end
上面代码,由于互相持有,导致pop此vc的时候,是不能执行dealloc的方法的。
方案一:weak strong dance 技术方案
为了避免循环引用所以引入weak弱化对象的策略,这样我们确实可以解决循环问题。我们来解决一下这。(后面只贴出一些相关核心变更代码)
__weak typeof(self) weakSelf = self;
self.myblock = ^{
weakSelf.name = @"小明";
};
self.myblock();
通过上面代码,weakSelf后,弱化了强引用,持有的时候不会造成引用计数+1,所以可以避开循环引用,pop后也发现,确实执行了dealloc方法,但是我们这样写就完美吗?
如下代码情况的时候,此种写法问题就会暴露出来了。
__weak typeof(self) weakSelf = self;
self.myblock = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
weakSelf.name = @"小明";
NSLog(@"名字:%@",weakSelf.name);
});
};
self.myblock();
我们开发过程中,block内经常放置一些耗时的代码,如果我们这么写的话,我们的weakSelf可能在执行耗时代码的时候,就已经被释放了,导致block内的weakSelf相关执行不下去,甚至引起crash。我们看一下相关打印结果。
2019-12-17 23:02:09.643525+0800 BlockTest[33548:5741676] 执行SecViewController释放
2019-12-17 23:02:10.222839+0800 BlockTest[33548:5741676] 名字:(null)
看出来了吧,这种情况下,我们的weakSelf已经被释放,我们不能使用weakSelf里面的属性了,这不是我们能接受的。那么如何才能实现,我们既要避免循环引用又要在block里使用相关对象呢? 这就用到我们的weak strong dance技术了
我们按照这个技术优化下相关代码。
__weak typeof(self) weakSelf = self;
self.myblock = ^{
__strong typeof(self) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
strongSelf.name = @"小明";
NSLog(@"名字:%@",weakSelf.name);
});
};
self.myblock();
我们查看一下相关输出
2019-12-17 23:09:17.107528+0800 BlockTest[33669:5761481] 名字:小明
2019-12-17 23:09:17.108047+0800 BlockTest[33669:5761481] 执行SecViewController释放
通过打印我们能发现,代码是在执行完我们block内的任务后,执行了释放。
那么weak strong dance 技术原理是什么呢?
我们在blcok代码块内部执行 __strong typeof(self) strongSelf = weakSelf;
的时候strongSelf
会作为局部变量会注册到autoreleasepool中,当前的block作用域执行结束的时候,我们会对这个自动释放池里面的对象进行统一释放。所以这就是为什么我们还能进行相关使用,而不造成循环引用的原因。
方案二:__block 合理设nil 破循环引用 技术方案
首先我们先写一个没有设置nil的代码,看看是否走dealloc
// 方法二: 合理设nil 破循环链
__block SecViewController *myVC = self;
self.myblock = ^{
myVC.name = @"小明";
};
self.myblock();
分别设置断点,并打印如下
(lldb) po self
<SecViewController: 0x7fc9b5f11280>
(lldb) po myVC
<SecViewController: 0x7fc9b5f11280>
我们发现这个指针一样,我们持有的循环也是一样的,也就是
self->block-->self这样一个循环。而结果也表明,并没有执行dealloc方法。所以我们考虑一下,在哪个位置进行设置nil。对这个循环引用进行断开呢?是不是我们可以将block持有的self在不用的时候进行nil设置就可以了呢?我们代码尝试一下。
// 方法二: 合理设nil 破循环链
__block SecViewController *myVC = self;
self.myblock = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
myVC.name = @"小明";
NSLog(@"名字:%@",myVC.name);
myVC = nil;
});
};
self.myblock();
我们在执行完之后,对这个self指向的内存地址也就是myVC设置为nil,从而避开了循环。我们看打印结果验证一下。
2019-12-18 00:25:26.770359+0800 BlockTest[34884:5959615] 名字:小明
2019-12-18 00:25:26.770581+0800 BlockTest[34884:5959615] 执行SecViewController释放
一切如我们猜测一样,完美避开循环持有。有人会问,为什么不能直接设置self = nil;
而非要经过一步myVC的过程,自己敲一下代码就知道了,self = nil的代码是爆红的。
方案三:传对象进入block 技术方案
我们再次反思,循环引用产生的原因,就是我们self持有block,而block闭包中持有self。为什么我们闭包中需要持有self呢?是因为我们需要使用self中的对象或者方法。那有没有一种可能,就是我们既能在闭包中使用到self,但是我们闭包又不持有这个self呢?如果能解决这个问题,循环链路就不会行成了。那到底有没有呢?我们是不是可以尝试传入闭包中使用的对象,从而避免循环呢?我们代码测试实现一下。
typedef void(^myBlock)(SecViewController *);
// 方法三: 通过传递参数进入block的闭包中,从而实现在闭包中对此对象的调用使用。我们更改一下block带入参数
self.myblock = ^(SecViewController *myVC) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
myVC.name = @"小明";
NSLog(@"名字:%@",myVC.name);
});
};
self.myblock(self);
我们查看一下输出结果
2019-12-18 00:49:24.632828+0800 BlockTest[35514:6024642] 名字:小明
2019-12-18 00:49:24.633076+0800 BlockTest[35514:6024642] 执行SecViewController释放
我们通过给block传入相关对象,用以完成闭包内的使用而不持有此对象的方式,也完成了循环链路的断开。
其它辅助知识:
关于自动变量捕获?
此处参考链接https://www.jianshu.com/p/00a7ee0177ea
Block捕获外部变量仅仅只捕获Block闭包里面会用到的值,其他用不到的值,它并不会去捕获。
自动变量是以值传递方式传递到Block的构造函数里面去的。Block只捕获Block中会用到的变量。由于只捕获了自动变量的值,并非内存地址,所以Block内部不能改变自动变量的值。Block捕获的外部变量可以改变值的是静态变量,静态全局变量,全局变量。上面例子也都证明过了。
堆上的Block会持有对象。我们把Block通过copy到了堆上,堆上也会重新复制一份Block,并且该Block也会继续持有该__block。当Block释放的时候,__block没有被任何对象引用,也会被释放销毁。
网友评论