美文网首页Block 探索之路
Block (初探2) 循环引用

Block (初探2) 循环引用

作者: 三月木头 | 来源:发表于2019-12-18 00:52 被阅读0次

    循环引用

    原因:
    互相持有 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不持有这个对象,但是我们还可以继续使用这个对象。

    1. 方法一: weak strong dance。 技术方案
    2. 方法二: __block 合理设nil 。 技术方案
    3. 方法三: 传对象进入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的代码是爆红的。

    Snip20191218_15.png
    方案三:传对象进入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没有被任何对象引用,也会被释放销毁。

    相关文章

      网友评论

        本文标题:Block (初探2) 循环引用

        本文链接:https://www.haomeiwen.com/subject/hmnsnctx.html