美文网首页ios 知识点iOS 笔记iOS 精集
Block循环引用的三种解决方式

Block循环引用的三种解决方式

作者: 穿山甲到底说了什么 | 来源:发表于2017-02-26 22:47 被阅读1162次

    今天 抽空看了下 *Objective-C高级编程iOS与OSX多线程和内存管理*,发现自己之前所理解的为什么block会发生循环引用?`有些理解是错误的,还好看了这个书,最后弄清楚了,希望写出来,既能算是一种总结,又能让其他小伙伴避免再遇到这个坑!下面 让我们一起来看几个场景!

    项目简单的类结构

    import "ViewController.h"
    #import "DetailViewController.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        DetailViewController *detailVC = [DetailViewController new];
        detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {
            
            NSLog(@"d: %@--", d);
        };
        [self presentViewController:d animated:YES completion:nil];
    }
    
    //---------------------DetailViewController-----------------------------
    @interface DetailViewController : UIViewController
    
    @property (nonatomic, copy) void(^testMemoryLeaksBLock)(DetailViewController *detailVC);
    
    @end
    
    @implementation DetailViewController
    
    -(void)dealloc {
        
        NSLog(@"dealloc");
    }
    
    - (void)viewDidDisappear:(BOOL)animated {
        
        [super viewDidDisappear:animated];
        !_testMemoryLeaksBLock ?: _testMemoryLeaksBLock(self);
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self dismissViewControllerAnimated:YES completion:nil];
    }
    
    

    场景一

    正如项目结构那样,当detailVC disappear时候,回调block是否会有内存泄漏呢?

     detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {
            
            NSLog(@"d: %@--", d);
        };
    
    局部变量不会造成,执行完自动释放,不会造成内存泄漏 - 1

    sow 正如上面分析图,一般情况下,是有内存泄漏的,但就是由于detailVC.testMemoryLeaksBLock所持有的d指针是一个局部变量,当block执行完之后,这个局部变量引用计数就为0,就被释放了,因此d 就不再持有detailVCdetailVC.testMemoryLeaksBLockdetailVC就不再持有, 循环引用被打破,还是会走 -[DetailViewController dealloc] 的.

    局部变量不会造成,执行完自动释放,不会造成内存泄漏 - 2

    更正:block不会强引用 block内部的局部变量weak弱指针,只会强引用 block 外部strong指针,并不是 block结束之后就会释放掉局部变量,所以不会引起循环,因为如果像那样说的话,假如block不执行,那局部变量岂不是就释放不掉了。

    具体看一下例1:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        Student *student = [[Student alloc]init];
        student.name = @"Hello World";
        
        __weak typeof(student) weakStu = student;
        Student *strongStu = weakStu;
        student.study = ^{
            
            //第1种写法
            //Student *strongStu = weakStu;
            //第2种写法
            //__strong typeof(weakStu) strongStu = weakStu;
            //第3种写法
            //typeof(student) strongStu = weakStu;
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                
                NSLog(@"my name is = %@", strongStu.name);
            });
        };
    }
    

    例1是有内存泄漏的没有走-[Student dealloc],因为未执行student.study, 所以dispatch_after block也不会走,但是dispatch_after bLock却强引用了strongStu还是发生了循环引用。这个比较好理解。但是下面例2,就改了一行代码 我怎么也想不通为什么 没有发生循环引用,走了-[Student dealloc] .

    例2:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        Student *student = [[Student alloc]init];
        student.name = @"Hello World";
        
        __weak typeof(student) weakStu = student;
        student.study = ^{
    
            /**
             * 三种写法是一样的效果,都是为了防止局部变量`student`在`viewDidLoad `之后销毁。如果不这样写的话,
             * 由于`student`局部变量被销毁,所以为nil,再走到`dispatch_after block`时候,由于weakStu是弱指针,
             * 所以不会强引用,最后打印为null,这不是我们想要的效果,`AFNetWorking`好多第三方库也是这么写的
             * 第1种写法
             * Student *strongStu = weakStu;
             * 第2种写法
             * __strong typeof(weakStu) strongStu = weakStu;
             * 第3种写法
             * typeof(student) strongStu = weakStu;
             */
            //随便取哪一种写法,这里取第一种写法
             Student *strongStu = weakStu;
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                
                NSLog(@"my name is = %@", strongStu.name);
            });
        };
    }
    

    dispatch_after block 强引用了外部变量strongStu ,这种不调用student.study()的写法为什么没有循环引用呢,但是如果在ViwDidLoad结尾调用student.study(),那么会在2秒后执行完dispatch_after block才会走-[Student dealloc],不就说明dispatch_after block持有这个student,走完才回销毁,那如果不执行student.study()的话,按道理讲,应该也会被dispatch_after block持有这个student,为什么 不会产生循环引用呢。

    匪夷所思如果有哪个大神路过,麻烦给个思路,我真想不明白。

    场景二

    block代码改成下面这样,当detailVC disappear时候,回调block是否又会有内存泄漏呢?

    DetailViewController *detailVC = [DetailViewController new]; 
    id  tmp = detailVC;
     detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {
            
         NSLog(@"d: %@--\nd: %@",  d,  tmp); 
       };
    

    答案:的确有内存泄漏 ,因为循环引用的问题,具体看下图:

    循环引用造成内存泄漏

    上面情况如果懂的话,下面这种写法是一样的,经典的循环引用
    detailVC持有testMemoryLeaksBLocktestMemoryLeaksBLock 持有 detailVC,导致两个引用计数都无法减一,最后谁也释放不了谁!!

    DetailViewController * detailVC = [DetailViewController new]; 
    detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {
            
         NSLog(@"detailVC: %@",  detailVC); 
       };
    

    如何解决内存泄漏

    原因我们了解了,现在是该如何解决了,下面根据之前场景逐一给出不同的思路,注意思路很重要,因为有很多种解决思路,都要搞清楚,举一反三,因为场景一没有内存泄漏,因此主要针对场景二

    针对场景二的解决方案1:

    有问题的代码:

    DetailViewController *detailVC = [DetailViewController new]; 
    id  tmp = detailVC;
    detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {
    
        NSLog(@"tmp: %@",  tmp); 
      };
    

    方案一的代码:

    DetailViewController * detailVC = [DetailViewController new];
     __block id tmp = detailVC;
     detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {
    
         NSLog(@"tmp: %@",  tmp); 
         tmp = nil;
     };
    

    虽然解决了内存泄漏,但是细心的看客姥爷肯定发现了这样写的一个弊端,没错,那就是 如果 detailVC.testMemoryLeaksBLock ()没有调用的话,还是会造成内存泄漏的,因为testMemoryLeaksBLock还是间接地强引用了detailVC, 算是一个思路吧,毕竟思路要广才能 想的更多,学的更多,不是吗!

    针对场景二的解决方案2:

    有问题的代码:

    DetailViewController *detailVC = [DetailViewController new]; 
    id  tmp = detailVC;
    detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {
    
        NSLog(@"tmp: %@",  tmp); 
      };
    

    方案二的代码:

    DetailViewController * detailVC = [DetailViewController new];
    __weak id weakTmp = detailVC;
     detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {
    
         NSLog(@"weakTmp: %@",  weakTmp); 
     };
    

    这也是平常开发中用得最多的一种解决循环引用的方法。来给小总结吧:方案一和方案二都是断掉testMemoryLeaksBLockdetailVC的强引用,自然可以;其实开发中还有一种方案也是可以的,那就是 断掉detailVCtestMemoryLeaksBLock 的强引用。

    针对场景二的解决方案3:

    有问题的代码:

    DetailViewController *detailVC = [DetailViewController new]; 
    id  tmp = detailVC;
    detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {
    
        NSLog(@"tmp: %@",  tmp); 
      };
    

    方案三的代码:

    @implementation DetailViewController
    
    -(void)dealloc {
        
        NSLog(@"dealloc");
    }
    
    - (void)viewDidDisappear:(BOOL)animated {
        
        [super viewDidDisappear:animated];
        !_ testMemoryLeaksBLock ?: _testMemoryLeaksBLock(self);
        //就加了下面一行代码也是可以的,因为一旦手动把 _testMemoryLeaksBLock置为空,  那么这个block就没有任何对象持有它,
        //换一句话说就是没有对象强引用这个block, 那么如果这个block之前在堆里,它就会被废弃掉,
        _testMemoryLeaksBLock= nil;
    }
    @end
    

    每一次执行完block之后都手动置nil,断掉detailVCtestMemoryLeaksBLock 的强引用也不失为一种方法。

    更正:block不会强引用 block内部的局部变量和 弱指针,只会强引用 block 外部strong指针,并不是 block结束之后就会释放掉局部变量,所以不会引起循环,因为如果像那样说的话,假如block不执行,那局部变量岂不是就释放不掉了。

    总结:这也是平常开发中三种解决循环引用的方法。希望大家周末都玩得开心,么么哒,下周准备写 GCD,有好多 线程同步的 知识,望 共同努力,共勉,手动!

    相关文章

      网友评论

      • 小五92:好多句式都读不通,而且前后有点不连贯
      • 3098930a0b72:第三种方案 把block回调置nil的情况,有肯能存在一定的问题,
        目前是放在diddisappear中, 那么dismiss之后一定是执行这个block回调以及置nil操作.
        在别的block回调触发中,有进入页面用户什么也不做,直接返回上级页面, 导致block不会执行回调的这种情况出现的.这时block也没办法nil吧.
        不知道我意思表达清楚了没有,也可能是我理解block不透彻, 如果说错了多包涵~
        小五92:只要页面dismiss ,就会执行diddisappear方法吧,为什么不能置nil呢?
      • Levi段玉磊:首先说下你对例1场景解答的问题
        你的解答是:
        Q:例1是有内存泄漏的没有走-[Student dealloc],因为未执行student.study, 所以dispatch_after block也不会走,但是dispatch_after bLock却强引用了strongStu还是发生了循环引用。这个比较好理解。但是下面例2,就改了一行代码 我怎么也想不通为什么 没有发生循环引用,走了-[Student dealloc] .
        A:
        我先更正下你的解答
        1.例1的内存泄漏和执行以及未执行student.study没有任何直接关联,如果未执行student.study存在内存泄漏,那么执行也会存在内存泄漏。
        2.但是dispatch_after bLock却强引用了strongStu还是发生了循环引用,这里面dispatch_after只是干扰因素,和你的内存泄漏没有直接关联。你可以去掉它,也一样会内存泄漏,所以去年的你应该是理解错了,可能今年的你已经解决这个问题了,不过还是针对去年的你说一下问题的原因吧。

        Q:例2,dispatch_after block 强引用了外部变量strongStu ,这种不调用student.study()的写法为什么没有循环引用呢,但是如果在ViwDidLoad结尾调用student.study(),那么会在2秒后执行完dispatch_after block才会走-[Student dealloc],不就说明dispatch_after block持有这个student,走完才回销毁,那如果不执行student.study()的话,按道理讲,应该也会被dispatch_after block持有这个student,为什么 不会产生循环引用呢。
        A:无论例1还是例2 dispatch_after的block都不会强引用你的变量,强引用变量的是strongStu.name。无论是否调用student.study()和你的内存泄漏没有直接关联。

        然后说一下我对例1的理解:
        例1之所以会内存泄漏是由于,student类中的的方法study是用copy属性修饰的,因此在你调用student.study = ^{ }时,是将strongStu.name这个强引用赋值到了block的堆中,block本来就被strongStu所引用,block又同时引用了StrongStu,导致A,B成为双向引用的保留环。
        __weak typeof(student) weakStu = student;
        Student *strongStu = weakStu;
        这两句话一起写,虽然不明白什么意思,不过weakStu是弱引用,storngStu是强引用。
        如果你想打破这个环,很简单,就是用弱引用就可以了。
        把例一这句话改成 NSLog(@"my name is = %@", weakStu.name);就不会出现内存泄漏了。

        然后说一下例2
        例2,为什么没有循环引用?因为你在block里面将弱引用变成了强引用,这个你应该会懂吧?那为什么例1的强引用就会循环引用,例2就不会呢?很简单,因为例1的StrongStu是block外面的强引用,这个是引用这个block的强引用,而例2的block里面的的强引用是临时变量,在block结束后会自动销毁掉。应该是没理解block外面的强引用和block里面的强引用导致不明白这两个例子的区别。
      • 西风颂:!_ testMemoryLeaksBLock ?: _testMemoryLeaksBLock(self);
        哥们写的这是什么意思啊?那第三种方案什么鬼啊?
        穿山甲到底说了什么:@西风颂 if (!_ testMemoryLeaksBLock ){
        }else {
        _testMemoryLeaksBLock(self);
        }
        第三种方案写的有把:“每一次执行完block之后都手动置nil,断掉detailVC对 testMemoryLeaksBLock 的强引用也不失为一种方法。”
      • sclcoder:block应该是copy外部的指针和引用还是有区别的
      • Haha小酱:谢谢亲的这篇文章,突然缕清思路了:+1::+1:
        Haha小酱:@穿山甲到底说了什么 :smile:大神回复的好快呀都不用写代码吗
        穿山甲到底说了什么:@Haha小酱 我写的有点问题,我待会再把文章更新下:smile:

      本文标题:Block循环引用的三种解决方式

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