《編寫(xiě)高質(zhì)量iOS與OS X代碼的52個(gè)有效方法》--第六章 第40條
(ps:此乃讀書(shū)筆記留瞳,加深記憶,僅供大家參考)
第40條:用塊引用其所屬對(duì)象時(shí)不要出現(xiàn)保留環(huán)
使用塊時(shí)肾筐,若不仔細(xì)思量,則很容易導(dǎo)致“保留環(huán)”(retain cycle)。在啟動(dòng)獲取器時(shí)瑞眼,可設(shè)置completion handler,這個(gè)塊會(huì)在下載結(jié)束之后以回調(diào)方式執(zhí)行棵逊。為了能在下載完成后通過(guò)p_requestCompleted方法執(zhí)行調(diào)用者所指定的塊這段代碼需要把completion handler保存到實(shí)例變量里面负拟。
typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
@property (nonatomic, strong, readonly) NSURL *url;
- (id)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
@end
@interface EOCNetworkFetcher()
@property (nonatomic, strong, readwrite) NSURL *url;
@property (nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler;
@property (nonatomic, strong) NSData *downloadData;
@end
@implementation EOCNetworkFetcher
- (instancetype)initWithURL:(NSURL *)url
{
if (self = [super init]) {
_url = url;
}
return self;
}
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion
{
self.completionHandler = completion;
//Start the request
//Request sets downloadedData property
//When request is finished, p_requestCompleted is called
}
- (void)p_requestCompleted
{
if (_completionHandler) {
_completionHandler(_downloadData);
}
}
某個(gè)類(lèi)可能會(huì)創(chuàng)建這種網(wǎng)絡(luò)數(shù)據(jù)獲取器對(duì)象,并用其從URL中下載數(shù)據(jù):
@implementation ViewController
{
EOCNetworkFetcher *_networkFetcher;
NSData *_fetchedData;
}
- (void)downloadData
{
NSURL *url = [NSURL URLWithString:@"www.baidu.com"];
_networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data) {
_fetchedData = data;
}];
}
因?yàn)閏ompletion handler塊要設(shè)置_fetchedData實(shí)例變量歹河,所以它必須捕獲self變量(變量捕獲詳見(jiàn)第37條)掩浙。這就是說(shuō)花吟,handler塊保留了創(chuàng)建網(wǎng)絡(luò)數(shù)據(jù)獲取器的那個(gè)EOCClass實(shí)例。而EOCClass實(shí)例則通過(guò)strong實(shí)例變量保留了獲取器厨姚,最后衅澈,獲取器對(duì)象又保留了handler塊。
要打破保留環(huán)也很容易:要么令_netwrokFetcher實(shí)例變量不再引用獲取器谬墙,要么令獲取器的completionHandler屬性不再持有handler塊今布。在網(wǎng)絡(luò)數(shù)據(jù)獲取器這個(gè)例子中,應(yīng)該等completion handler塊執(zhí)行完畢之后拭抬,再去打破保留環(huán)部默,以便使獲取器對(duì)象在handler塊執(zhí)行期間保持存活狀態(tài)。比方說(shuō)造虎,completion handler塊的代碼可以這么修改:
[_networkFetcher startWithCompletionHandler:^(NSData *data) {
_fetchedData = data;
_networkFetcher = nil;
}];
在本例中傅蹂,唯有completion handler運(yùn)行過(guò)后,方能解除保留環(huán)算凿。若是completion handler一直不運(yùn)行份蝴,那么保留環(huán)就無(wú)法打破,于是內(nèi)存就會(huì)泄露氓轰。
像completion handler塊這種寫(xiě)法婚夫,還可能引入另外一種形式的保留環(huán)。如果completion handler塊所引用的對(duì)象最終又引用了這個(gè)塊本身署鸡,那么就會(huì)出現(xiàn)保留環(huán)案糙。
- (void)downloadData
{
NSURL *url = [NSURL URLWithString:@"www.baidu.com"];
EOCNetworkFetcher *networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[networkFetcher startWithCompletionHandler:^(NSData *data) {
_fetchedData = data;
}];
}
而這次比剛才那個(gè)例子更難于發(fā)覺(jué),completion handler塊其實(shí)要通過(guò)獲取器對(duì)象來(lái)引用其中的URL靴庆。于是时捌,塊就要保留獲取器,而獲取器反過(guò)來(lái)又經(jīng)由其completionHandler屬性保留了這個(gè)塊撒穷∠灰回想一下,獲取器對(duì)象之所以要把completionHandler塊保存在屬性里面端礼,其唯一目的就是想稍后使用這個(gè)塊禽笑。可是蛤奥,獲取器一旦運(yùn)行過(guò)completion handler之后佳镜,就沒(méi)有必要在保留它了。
- (void)p_requestCompleted
{
if (_completionHandler) {
_completionHandler(_downloadData);
self.completionHandler = nil;
}
}
這樣一來(lái)凡桥,只要下載請(qǐng)求執(zhí)行完畢蟀伸,保留環(huán)就解除了,而獲取器對(duì)象也將會(huì)在必要時(shí)為系統(tǒng)所回收。請(qǐng)注意啊掏,之所以把completion handler暴露為獲取對(duì)象的公共屬性蠢络,那么就不便在執(zhí)行完下載請(qǐng)求之后直接將其清理掉了,因?yàn)榧热灰呀?jīng)把handler作為屬性公布了迟蜜,那就意味著調(diào)用者可以自由使用它刹孔,若是此時(shí)有在內(nèi)部將其清理掉的話(huà),則會(huì)破壞“封裝語(yǔ)義”(encapsulation semantic)娜睛。在這種情況下要想打破保留環(huán)髓霞,只有一個(gè)辦法可用,那就是強(qiáng)迫調(diào)用者在handler代碼里自己把completionHandler屬性清理干凈畦戒》娇猓可這并不十分合理,因?yàn)槟銦o(wú)法假定調(diào)用者一定會(huì)這么做障斋,他們反過(guò)來(lái)會(huì)抱怨你沒(méi)把內(nèi)存泄漏問(wèn)題處理好纵潦。
要點(diǎn)
- 如果塊所捕獲的對(duì)象直接或間接地保留了塊本身,那么就得當(dāng)心保留環(huán)問(wèn)題配喳。
- 一定要找個(gè)適當(dāng)?shù)臅r(shí)機(jī)解除保留環(huán)酪穿,而不能把責(zé)任推給API的調(diào)用者凳干。