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

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

1.block为何要用copy修饰

1.1 内存堆栈理解

  • 内存栈区

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

  • 内存堆区

通常由程序员分配释放,若程序员不释放,程序结束时可能由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。ide

    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的时候返回的都是一个拷贝,不论是变量、对象仍是指针都是返回拷贝,可是这个拷贝是浅拷贝。在这里我须要理解如下两点:atom

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

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

解决方案:就是在返回的时候,把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.参考

相关文章
相关标签/搜索