寫這篇文章的原因是第一次面試時被問到了block循環引用的問題,當時回答的不是很好,首先要明確的是,block是否用copy修飾決定不了循環引用的產生,在此再一次進行補強,有不對的地方還請多多指教。html
由編譯器自動分配釋放,存放函數的參數值,局部變量的值等,不須要程序員來操心。其操做方式相似於數據結構中的棧。ios
通常由程序員分配釋放,若程序員不釋放,程序結束時可能由OS回收。儘管後邊蘋果引入了ARC機制,可是ARC的機制其實僅僅是系統幫助程序員添加了retain,release,autorelease代碼,並非說系統就能夠自動管理了。他的系統管理的原理仍是MRC,並無本質區別。注意內存堆區與數據結構中的堆是兩回事,分配方式卻是相似於鏈表。程序員
首先,block是一個對象,因此block理論上是能夠retain/release的。可是block在建立的時候它的內存是默認是分配在棧(stack)上,而不是堆(heap)上的。因此它的做用域僅限建立時候的當前上下文(函數, 方法...),當你在該做用域外調用該block時,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的循環引用,將在下面進行討論。函數
關於函數返回,在一個函數的內部,return的時候返回的都是一個拷貝,不論是變量、對象仍是指針都是返回拷貝,可是這個拷貝是淺拷貝。在這裏我須要理解如下兩點:atom
明確上邊兩點以後,咱們再來講,在MRC下,若是一個block做爲參數,沒有通過copy就返回。後果是什麼呢?因爲return的時候返回的是淺拷貝,也就是說返回的是對象的地址,由於在返回後這個block對應的棧內存就銷燬了。若是你屢次調用這個block就會發現,程序會崩潰。崩潰緣由就是上邊所說,block佔用的空間已經釋放了,你不能夠進行訪問了。spa
解決方案:就是在返回的時候,把block進行拷貝做爲參數進行返回。這樣作的好處是返回的那個block存儲空間是在堆內,堆內的空間須要程序員本身去釋放,系統不會自動回收,也就不會出現訪問已釋放內存致使的崩潰了。也就是咱們在MRC下須要使用copy修飾符的緣由。(此處是不是經過深複製在堆中申請內存不求甚解,在此標記,繼續深究)
首先前面講的內容都是在MRC下,MRC下block須要用copy修飾,可是在ARC下使用copy或strong修飾其實都同樣,由於block的retain就是用copy來實現的。
在開始以前咱們須要明確一點:是否是全部的block,使用self都會出現循環引用?其實否則,系統和第三方框架的block絕大部分不會出現循環引用,只有少數block以及咱們自定義的block會出現循環引用。而咱們只要抓住本質緣由就能夠了,以下:
若是block沒有直接或者間接被self存儲,就不會產生循環引用。就不須要用weak self。(retainCount沒法變爲0)
因爲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就會持有該對象而不是該對象中的某個屬性或者函數。
間接強引用中,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永遠都不會被釋放掉產生常駐內存。
使用通知(NSNotifation),調用系統自帶的Block,在Block中使用self會發生循環引用。
注:自定義的block出現循環引用時都會出現警告,因此出問題時容易解決。但在這裏,在block中的確出現了循環引用,也的確沒有出現警告,這纔是咱們真正須要注意的,也是爲何咱們須要理解block循環引用的緣由。
通常性解決辦法
__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;
};
複製代碼
缺陷
若是我想在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內部方法執行完畢以後,延時器和對象都被銷燬,這樣就完美實現了咱們的需求。
黑色表明強引用,綠色表明弱引用
解決辦法二
- (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。