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.參考

相關文章
相關標籤/搜索