block的一些基本使用,我已經(jīng)寫了一篇,有興趣的話也可以去看看,block在開發(fā)中給我們帶了很多方便,然而使用block也有一些隱患,例如容易出現(xiàn)循環(huán)引用導(dǎo)致內(nèi)存泄露,這篇文章會剖析為什么會出現(xiàn)這種問題,如何去解決,各個對象在內(nèi)存中釋放的時機(jī)等一些問題。
1.為什么會出現(xiàn)循環(huán)引用
解釋這個問題我會用一個小例子和圖示分析,請看事例:
@implementation RequestUtil
- (void)getRequestData{
[self.requester startWithCompletionHandler:^(NSData *data){
_fetchData = data;
}];
}
@end
假設(shè)requester和fetchData是RequestUtil這個類的兩個屬性,這段代碼片段貌似完全沒問題,然而它已經(jīng)出現(xiàn)循環(huán)引用了,那么上面的這段代碼內(nèi)存關(guān)系圖會是下面圖示的關(guān)系:
對象self強(qiáng)引用著對象requester,requester執(zhí)行startWithCompletionHandler方法的時候,會對block進(jìn)行強(qiáng)引用,在block中要訪問對象self中的屬性,首先得對對象self強(qiáng)引用,只有引用了self才能訪問對象self中的屬性,這樣一來就出現(xiàn)一個閉環(huán),誰也無法釋放,這塊內(nèi)存一直被占用著,從而導(dǎo)致內(nèi)存泄露。
2. 如何解決循環(huán)引用
解決循環(huán)引用的思路是打破上圖所示的閉環(huán),下面介紹兩種方法解決這個問題。
1.解決方案一:
@implementation RequestUtil
- (void)getRequestData{
__weak typeof(self) weakSelf = self;
[self.requester startWithCompletionHandler:^(NSData *data{
weakSelf.fetchData = data;
}];
}
@end
上面這段代碼就完美的解決了循環(huán)引用的問題,這段代碼在內(nèi)存中的關(guān)系如圖:
通過創(chuàng)建一個weak類型的指針指向self對象,block去強(qiáng)引用這個weakSelf就不會出現(xiàn)循環(huán)引用了。在內(nèi)存中釋放的順序是,先self釋放,self釋放由于沒有任何對象強(qiáng)引用requester,requester隨著釋放,之后block釋放,weakSelf也沒有任何對象引用,也從內(nèi)存中釋放。
2.解決方案二:
在合適的地方,手動將block或者requester釋放掉,打破閉環(huán)。
- 手動釋放requester
@implementation RequestUtil
- (void)getRequestData{
[self.requester startWithCompletionHandler:^(NSData *data){
_fetchData = data;
_requester = nil;
}];
}
@end
上面這段代碼解決了循環(huán)引用的問題,這段代碼在內(nèi)存中的關(guān)系如圖:
當(dāng)requester指向nil的時候,它所引用的block由于沒有任何對象引用它,block會在內(nèi)存中被釋放掉,block被釋放后不再強(qiáng)引用self,當(dāng)沒有其它指針強(qiáng)引用self的時候,self會被釋放,requester也就隨著釋放,從而都從內(nèi)存中釋放了。
- 手動釋放blcok
@implementation RequestUtil
- (void)getRequestData{
[self.requester startWithCompletionHandler:^(NSData *data){
_fetchData = data;
}];
}
@end
@implementation XXXXXXX
- (void)startWithCompletionHandler:(void (^)(NSData *data)) block{
// do something
NSData *pngData = UIImagePNGRepresentation([UIImage imageNamed:@"pngName"]);
block(pngData);
block = nil; // 一定要確保block執(zhí)行完畢才可這么做,如果block中又有多線程,那么這么做就不合適了,總之block要在合適的時間釋放
}
@end
上面這段代碼也解決了循環(huán)引用的問題,這段代碼在內(nèi)存中的關(guān)系如圖:
和手動釋放requester一個道理,將block指向nil,self會在需要釋放的時候釋放,requester也隨著釋放,從而都從內(nèi)存中釋放了。
3.棧塊和堆塊
定義塊的時候,其所占的內(nèi)存是分配在棧區(qū)的,也就是說,塊只在定義它的那個范圍有效。請看事例:
void (^block)();
if(flag){
block = ^{ // do something };
} else {
block = ^{ // do something };
}
block();
這段代碼看上去似乎很OK,其實是有風(fēng)險的,定義在if和else中的兩個block都分配在棧內(nèi)存中,等離開了相應(yīng)的if語句塊或else語句塊范圍之后,編譯器有可能把分配給block的內(nèi)存覆寫掉。于是,這兩個塊只能保證在對應(yīng)的if或else語句范圍內(nèi)有效。這樣的代碼編譯完全沒問題的,運行起來就看人品了,若編譯器未覆寫block在棧區(qū)所占用的內(nèi)存,則程序可以正常運行,若覆寫或回收了程序會崩潰。
解決辦法是給塊發(fā)送copy消息,使之拷貝到堆內(nèi)存中,這樣塊就成了帶引用計數(shù)的對象了。這也是為什么block要用copy修飾的原因
void (^block)();
if(flag){
block = [^{ // do something } copy];
} else {
block = [^{ // do something } copy];
}
block();
這樣代碼就安全了,如果是采用非ARC的,那么最后記得手動釋放塊在堆區(qū)所分配的內(nèi)存。