美文网首页iOS开发精进block
iOS开发笔记(二):block循环引用

iOS开发笔记(二):block循环引用

作者: 天空当被子 | 来源:发表于2017-12-28 19:52 被阅读118次

    写这篇文章的缘由是第一次面试时被问到了block循环引用的问题,当时回答的不是很好,首先要明确的是,block是否用copy修饰决定不了循环引用的产生,在此再一次进行补强,有不对的地方还请多多指教。

    1.block为什么要用copy修饰

    1.1 内存堆栈理解

    • 内存栈区

    由编译器自动分配释放,存放函数的参数值,局部变量的值等,不需要程序员来操心。其操作方式类似于数据结构中的栈。

    • 内存堆区

    一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。尽管后边苹果引入了ARC机制,但是ARC的机制其实仅仅是系统帮助程序员添加了retain,release,autorelease代码,并不是说系统就可以自动管理了。他的系统管理的原理还是MRC,并没有本质区别。注意内存堆区与数据结构中的堆是两回事,分配方式倒是类似于链表。

    1.2 block作用域

    首先,block是一个对象,所以block理论上是可以retain/release的。但是block在创建的时候它的内存是默认是分配在栈(stack)上,而不是堆(heap)上的。所以它的作用域仅限创建时候的当前上下文(函数, 方法...),当你在该作用域外调用该block时,block占用的内存已经释放,无法进行访问,程序就会崩溃,出现野指针错误。

    1.3 三种block

    • NSGlobalBlock:全局的静态block,没有访问外部变量,存储在代码区(存储方法或者函数)。他直到程序结束的时候,才会被被释放。但是我们实际操作中基本上不会使用到不访问外部变量的block。

        void(^testOneBlock)() = ^(){
            NSLog(@"我是全局的block");
        };
        NSLog(@"testOneBlock=%@",testOneBlock);
        //控制台输出
        2017-06-10 09:45:09.767 ReactiveCocoa[871:14517] testOneBlock=<__NSGlobalBlock__: 0x1045982d0>
        //全局block,他会随程序销毁而销毁
      
    • NSStackBlock:保存在栈中的block,没有用copy去修饰并且访问了外部变量。但是必须要在MRC的模式下控制台才会输出NSStackBlock类型。

        //需要MRC模式
        int a = 5;
        void(^testTwoBlock)() = ^(){
            NSLog(@"%d",a);
        };
        NSLog(@"testTwoBlock=%@",testTwoBlock);
        //控制台输出
        2017-06-10 09:45:09.768 ReactiveCocoa[871:14517] testTwoBlock=<__NSStackBlock__: 0x7fff5b668770>
        //栈区block,函数调用完毕就会销毁
      
    • NSMallocBlock:保存在堆中的block,此类型blcok是用copy修饰出来的block,它会随着对象的销毁而销毁,只要对象不销毁,我们就可以调用的到在堆中的block。

        int a = 5;
        self.block1 = ^(NSString *str, UIColor *color){
            NSLog(@"%d",a);
        };
        NSLog(@"block1=%@",self.block1);
        //控制台输出
        2017-06-10 10:02:35.107 ReactiveCocoa[1075:19674] block1=<__NSMallocBlock__: 0x60000004ee50>
        //用copy修饰的不会函数调用完就结束,随对象销毁才销毁,这种是在开发中正确使用block的姿势
      

      第三种block在有些情况下会造成block的循环引用,将在下面进行讨论。

    1.4 另一种理解方式:函数返回

    关于函数返回,在一个函数的内部,return的时候返回的都是一个拷贝,不管是变量、对象还是指针都是返回拷贝,但是这个拷贝是浅拷贝。在这里我需要理解以下两点:

    • 对于直接返回一些基本类型的变量来说,直接返回值的拷贝就好,没有问题。
    • 对于返回一些非动态分配(new/malloc)得到的指针就可能出现问题,因为尽管你返回了这个指针地址。但是这个指针可能指向的栈内存,栈内存在函数执行完毕后就自动销毁了。如果销毁之后你再去访问,就会访问坏内存会导致程序崩溃。

    明确上边两点之后,我们再来说,在MRC下,如果一个block作为参数,没有经过copy就返回。后果是什么呢?由于return的时候返回的是浅拷贝,也就是说返回的是对象的地址,因为在返回后这个block对应的栈内存就销毁了。如果你多次调用这个block就会发现,程序会崩溃。崩溃原因就是上边所说,block占用的空间已经释放了,你不可以进行访问了。

    解决方案:就是在返回的时候,把block进行拷贝作为参数进行返回。这样做的好处是返回的那个block存储空间是在堆内,堆内的空间需要程序员自己去释放,系统不会自动回收,也就不会出现访问已释放内存导致的崩溃了。也就是我们在MRC下需要使用copy修饰符的原因。(此处是否是通过深复制在堆中申请内存不求甚解,在此标记,继续深究)

    1.5 ARC下block用什么修饰

    首先前面讲的内容都是在MRC下,MRC下block需要用copy修饰,但是在ARC下使用copy或strong修饰其实都一样,因为block的retain就是用copy来实现的。

    2.block循环引用

    在开始之前我们需要明确一点:是不是所有的block,使用self都会出现循环引用?其实不然,系统和第三方框架的block绝大部分不会出现循环引用,只有少数block以及我们自定义的block会出现循环引用。而我们只要抓住本质原因就可以了,如下:

    如果block没有直接或者间接被self存储,就不会产生循环引用。就不需要用weak self。(retainCount无法变为0)

    2.1 直接强引用:self -> block -> self

    由于block会对block中的对象进行持有操作,就相当于持有了其中的对象,而如果此时block中的对象又持有了该block,则会造成循环引用。如下

    typedef void(^block)();
    
    @property (copy, nonatomic) block myBlock;
    @property (copy, nonatomic) NSString *blockString;
    
    - (void)testBlock {
        self.myBlock = ^() {
            //其实注释中的代码,同样会造成循环引用
            NSString *localString = self.blockString;
            //NSString *localString = _blockString;
            //[self doSomething];
        };
    }
    

    注:以下调用注释掉的代码同样会造成循环引用,因为不管是通过self.blockString还是_blockString,或是函数调用[self doSomething],因为只要block中用到了对象的属性或者函数,block就会持有该对象而不是该对象中的某个属性或者函数。

    强引用1 强引用2

    2.2 间接强引用:self -> 某个类 -> block -> self

    间接强引用中,self并没有直接拥有block属性。来看下面一个例子:

    这是一个持有block的view: XXSubmitBottomView

    typedef void(^BtnPressedBlock)(UIButton *btn);
    
    @interface XXSubmitBottomView : UIView
    
    @property(strong,nonatomic)UILabel *allPriceLab;
    @property(strong,nonatomic)UIButton *submittBtn;
    @property(nonatomic, weak)XXConfirmOrderController *currentVc;
    @property(nonatomic, weak)XXConfimOrderModel *model;
    
    @property(nonatomic, copy)BtnPressedBlock block;
    -(void)submittBtnPressed:(BtnPressedBlock)block;
    

    这是一个持有bottomView属性的控制器: XXConfirmOrderController

    @interface XXConfirmOrderController ()
    
    @property(nonatomic, strong) XXConfimOrderTableView *tableView;
    @property(nonatomic, strong) XXSubmitBottomView *bottomView;
    @property(nonatomic, strong) XXConfimOrderModel *confimModel;
    
    @end
    
    @implementation XXConfirmOrderController
    
    -(void)viewDidLoad{
        [super viewDidLoad];
        self.title = @"确认下单";
        self.view.backgroundColor = DDCJ_Gray_Color;
    
        //UI
        [self.view addSubview:self.tableView];
        [self.view addSubview:self.bottomView];
    
        //Data
        [self loadData];
    }
    

    下面是self.bottomView的懒加载以及block的回调处理

    -(XXSubmitBottomView *)bottomView{
        if (!_bottomView) {
            _bottomView = [[XXSubmitBottomView alloc] initWithFrame:CGRectMake(0, self.view.height - 50, Width, 50)];
            _bottomView.currentVc = self;
        
        
    #warning self.bottomView.block  self间接持有了BtnPressedBlock 必须使用weak!
        
            WEAKSELF  //ps: weakSelf的宏定义#define WEAKSELF typeof(self) __weak weakSelf = self;
       
        
            [_bottomView submittBtnPressed:^(UIButton *btn) {
            
                NSLog(@"do提交订单");
                
                MBProgressHUD *hud = [MBProgressHUD showMessage:@"加载中..." toView:weakSelf.view];
            
                NSMutableDictionary *dynamic = [NSMutableDictionary dictionary];
                [dynamic setValue:weakSelf.confimModel.orderRemark forKey:@"orderRemark"];
                if (weakSelf.agreementId) {
                    [dynamic setValue:weakSelf.agreementId forKey:@"agreementId"];
                }
                if (weakSelf.isShoppingCartEnter) {
                    [dynamic setValue:@"0" forKey:@"orderOrigin"];
                }else{
                    [dynamic setValue:@"1" forKey:@"orderOrigin"];
                }
                        
                [[APIClientFactory sharedManager] requestConfimOrderWithDynamicParams:dynamic success:^(NSMutableArray *dataArray) {
                
                    [hud hideAnimated:YES];                
                    [weakSelf handlePushControllerWithModelList:dataArray];
                
                } failure:^(NSError *error) {
                    [hud hideAnimated:YES];
                    [MBProgressHUD showError:error.userInfo[@"message"]];
                }];
            }];
        }
    
        return _bottomView;
    }
    

    此处的控制器self并没有直接持有block属性,但是却强引用了bottomView,bottomView强引用了block属性,这就造成了间接循环引用。block回调内必须使用[weak self]来打破这个循环,否则就会导致这个控制器self永远都不会被释放掉产生常驻内存。

    2.3 实际开发中的循环引用

    使用通知(NSNotifation),调用系统自带的Block,在Block中使用self会发生循环引用。

    twoVC发送通知 --> 给oneVC oneVC 接收通知 使用通知-发生循环引用

    注:自定义的block出现循环引用时都会出现警告,所以出问题时容易解决。但在这里,在block中的确出现了循环引用,也的确没有出现警告,这才是我们真正需要注意的,也是为什么我们需要理解block循环引用的原因。

    2.4 解决办法

    • 一般性解决办法

        __weak typeof(self) weakSelf = self;
      

      通过__weak的修饰,先把self弱引用(默认是强引用,实际上self是有个隐藏的__strong修饰的),然后在block回调里用weakSelf,这样就会打破保留环,从而避免了循环引用,如下:

        self -> block -> weakSelf
        self -> 某个类 -> block ->weakSelf
      

      提醒:__block与__weak都可以用来解决循环引用,但是,__block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型。__weak只能在ARC模式下使用,也只能修饰对象(NSString),不能修饰基本数据类型(int)。__block对象可以在block中被重新赋值,__weak不可以。

    • @weakify

        @weakify(self)
        self.myBlock = ^() {
            NSString *localString = self.blockString;
        };
      
    弱引用1
    弱引用2

    2.5 weak的缺陷

    • 缺陷

      如果我想在Block中延时来运行某段代码,这里就会出现一个问题,看这段代码:

        - (void)viewDidLoad {
            [super viewDidLoad];
            MitPerson*person = [[MitPerson alloc]init];
            __weak MitPerson * weakPerson = person;
            person.mitBlock = ^{
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    [weakPerson test];
                });
            };
            person.mitBlock();
        }
      

      直接运行这段代码会发现[weakPerson test];并没有执行,打印一下会发现,weakPerson已经是 Nil 了,这是由于当我们的viewDidLoad方法运行结束,由于是局部变量,无论是MitPerson和weakPerson都会被释放掉,那么这个时候在Block中就无法拿到正真的person内容了。

    • 解决办法一

        - (void)viewDidLoad {
            [super viewDidLoad];
            MitPerson*person = [[MitPerson alloc]init];
            __weak MitPerson * weakPerson = person;
            person.mitBlock = ^{
                __strong MitPerson * strongPerson = weakPerson;
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    [strongPerson test];
                });
            };
            person.mitBlock();
        }
      

      这样当2秒过后,计时器依然能够拿到想要的person对象。

      深入理解

      • 首先了解一些概念:

        堆里面的block(被copy过的block)有以下现象:

        1.block内部如果通过外面声明的强引用来使用,那么block内部会自动产生一个强引用指向所使用的对象。

        2.block内部如果通过外面声明的弱引用来使用,那么block内部会自动产生一个弱引用指向所使用的对象。

      • 这段代码的目的:

        • 首先,我们需要在Block块中调用,person对象的方法,既然是在Block块中我们就应该使用弱指针来引用外部变量,以此来避免循环引用。但是又会出现问题,什么问题呢?就是当我计时器要执行方法的时候,发现对象已经被释放了。

        • 接下来就是为了避免person对象在计时器执行的时候被释放掉:那么为什么person对象会被释放掉呢?因为无论我们的person强指针还是weakPerson弱指针都是局部变量,当执行完ViewDidLoad的时候,指针会被销毁。对象只有被强指针引用的时候才不会被销毁,而我们如果直接引用外部的强指针对象又会产生循环引用,这个时候我们就用了一个巧妙的代码来完成这个需求。

        • 首先在person.mitBlock引用外部weakPerson,并在内部创建一个强指针去指向person对象,因为在内部声明变量,Block是不会强引用这个对象的,这也就在避免的person.mitBlock循环引用风险的同时,又创建出了一个强指针指向对象。

        • 之后再用GCD延时器Block来引用相对于它来说是外部的变量strongPerson,这时延时器Block会默认创建出来一个强引用来引用person对象,当person.mitBlock作用域结束之后strongPerson会跟着被销毁,内存中就仅剩下了延时器Block强引用着person对象,2秒之后触发test方法,GCD Block内部方法执行完毕之后,延时器和对象都被销毁,这样就完美实现了我们的需求。

          黑色代表强引用,绿色代表弱引用

          Block循环引用
    • 解决办法二

        - (void)viewDidLoad {
            [super viewDidLoad];
            MitPerson*person = [[MitPerson alloc]init];
            @weakify(self)
            person.mitBlock = ^{
                @strongify(self)
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    [self test];
                });
            };
            person.mitBlock();
        }
      

      可以看出,这样就完美解决了weak的缺陷,我们可以在block中随意使用self。

    3.参考

    相关文章

      网友评论

      • 小七月TuTu:__block的相关说法中 提醒:__block与__weak都可以用来解决循环引用,但是,__block不管是ARC还是MRC模式下都可以使用
        是不对的__block 在MRC 下是可以解决循环引用的但是在ARC 环境下 __block 修饰对象 被强引用 也就是 对象持有Block Block 持有__block变量 __block 变量持有对象.造成大环引用MRC 下不会
      • GeniusWong:@weakify() 这个是个宏定义吗

      本文标题:iOS开发笔记(二):block循环引用

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