在Objective-C這種面向?qū)ο笳Z言里夫壁,內(nèi)存管理是個重要概念。要想用一門語言寫出內(nèi)存使用效率高而且又沒有bug的代碼庆械,就得掌握其內(nèi)存模型的種種細(xì)節(jié)薇溃。
一旦理解了這些規(guī)則,你就會發(fā)現(xiàn)缭乘,其實Objective-C的內(nèi)存管理沒那么復(fù)雜沐序,而且有了"自動引用計數(shù)"(Automatic Reference Counting, ARC)之后,就變得更為簡單了堕绩。ARC幾乎把所有內(nèi)存管理事宜都交由編譯器來決定策幼,開發(fā)者只需專注于業(yè)務(wù)邏輯。
Objective-C語言使用引用計數(shù)來管理內(nèi)存奴紧,也就是說特姐,每個對象都有個可以遞增或遞減的計數(shù)器。如果想使某個對象繼續(xù)存活黍氮,那就遞增其引用計數(shù)唐含;用完了之后,就遞減其計數(shù)滤钱。計數(shù)變?yōu)?觉壶,就表示沒人關(guān)注此對象了脑题,于是件缸,就可以把它銷毀。上面這幾句話只是個概述叔遂,要想寫出優(yōu)秀的Objective-C代碼他炊,必須完全理解此問題才行,即便打算用ARC來編碼(參見第30條)也是如此已艰。
從Mac OS X 10.8開始痊末,"垃圾收集器"(garbage collector)已經(jīng)正式廢棄了,以O(shè)bjective-C代碼編寫Mac OS X程序時不應(yīng)再使用它哩掺,而iOS則從未支持過垃圾收集凿叠。因此,掌握引用計數(shù)機制對于學(xué)好Objective-C來說十分重要。Mac OS X程序已經(jīng)不能再依賴?yán)占髁撕屑鴌OS系統(tǒng)不支持此功能蹬碧,將來也不會支持。
已經(jīng)用過ARC的人可能會知道: 所有與引用計數(shù)有關(guān)的方法都無法編譯炒刁,然而現(xiàn)在先暫時忘掉這件事恩沽。那些方法確實無法用在ARC中,不過本條就是要從Objective-C的角度講解引用計數(shù)翔始,而ARC實際上也是一種引用計數(shù)機制罗心,所以,還是要談?wù)勥@些在開啟ARC功能時不能直接調(diào)用的方法城瞎。
引用計數(shù)工作原理
在引用計數(shù)架構(gòu)下渤闷,對象有個計數(shù)器,用以表示當(dāng)前有多少個事物想令此對象繼續(xù)存活下去脖镀。這在Objective-C中叫做"保留計數(shù)"(retain count)肤晓,不過也可以叫"引用計數(shù)"(reference count)。NSObject協(xié)議聲明了下面三個方法用于操作計數(shù)器认然,以遞增或遞減其值:
- Retain 遞增保留計數(shù)补憾。
- release 遞減保留計數(shù)。
- autorelease 待稍后清理"自動釋放池"(autorelease pool)時卷员,再遞減保留計數(shù)盈匾。
查看保留計數(shù)的方法叫做retainCount,此方法不太有用毕骡,即便在調(diào)試時也如此削饵,所以筆者(與蘋果公司)并不推薦大家使用這個方法。更多內(nèi)容請參閱第36條未巫。
對象創(chuàng)建出來時窿撬,其保留計數(shù)至少為1.若想令其繼續(xù)存活,則調(diào)用retain方法叙凡。要是某部分代碼不再使用此對象劈伴,不想令其繼續(xù)存活,那就調(diào)用release或autorelease方法握爷。最終當(dāng)保留計數(shù)歸零時跛璧,對象就回收了(deallocated),也就是說新啼,系統(tǒng)會將其占用的內(nèi)存標(biāo)記為"可重用"(reuse)追城。此時,所有指向該對象的引用也都變得無效了燥撞。
下圖演示了對象自創(chuàng)建出來之后歷經(jīng)一次"保留"及兩次"釋放"操作的過程座柱。
應(yīng)用程序在其生命期中會創(chuàng)建很多對象迷帜,這些對象都相互聯(lián)系著。例如色洞,表示個人信息的對象會引用另一個表示人名的字符串對象瞬矩,而且可能還會引用其他個人信息對象,比如在存放朋友的set中就是如此锋玲,于是景用,這些相互關(guān)聯(lián)的對象就構(gòu)成了一張"對象圖"(object graph)。對象如果持有指向其他對象的強引用(strong reference)惭蹂,那么前者就"擁有"(own)后者伞插。也就是說,對象想令其所引用的那些對象繼續(xù)存活盾碗,就可將其"保留"媚污。等用完了之后,再釋放廷雅。
在下圖所示的對象圖中耗美,ObjectB與ObjectC都引用了
ObjectA。若ObjectB與ObjectC都不再使用ObjectA航缀,則其保留計數(shù)降為0商架,于是便可摧毀了。還有其他對象想令ObjectB與ObjectC繼續(xù)存活芥玉,而應(yīng)用程序里又有另外一些對象想令那些對象繼續(xù)存活蛇摸。如果按"引用樹"回溯,那么最終會發(fā)現(xiàn)一個"根對象"(root object)灿巧。在Mac OS X應(yīng)用程序中赶袄,此對象就是NSApplication對象;而在iOS應(yīng)用程序中抠藕,則是UIApplication對象饿肺。兩者都是應(yīng)用程序啟動時創(chuàng)建的單例。
下面這段代碼有助于理解這些方法的用法:
NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
// do something with `array’
[array release];
如前所述盾似,由于代碼中直接調(diào)用了release方法敬辣,所以在ARC下無法編譯。在Objective-C中颜说,調(diào)用alloc方法所返回的對象由調(diào)用者所擁有购岗。也就是說,調(diào)用者已通過alloc方法表達(dá)了想令該對象繼續(xù)存活下去的意愿门粪。不過請注意,這并不是說對象此時的保留計數(shù)必定是1烹困。在alloc或"initWithInt:"方法的實現(xiàn)代碼中玄妈,也許還有其他對象也保留了此對象,所以,其保留計數(shù)可能會大于1.能夠肯定的是: 保留計數(shù)至少為1.保留計數(shù)這個概念就應(yīng)該這樣來理解才對拟蜻。絕不應(yīng)該說保留計數(shù)一定是某個值绎签,只能說你所執(zhí)行的操作是遞增了該計數(shù)還是遞減了該計數(shù)。
創(chuàng)建完數(shù)組后酝锅,把number對象加入其中诡必。調(diào)用數(shù)組的"addObject:"方法時,數(shù)組也會在number上調(diào)用retain方法搔扁,以期繼續(xù)保留此對象爸舒。這是,保留計數(shù)至少為2稿蹲。接下來扭勉,代碼不再需要number對象了,于是將其釋放】疗福現(xiàn)在的保留計數(shù)至少為1涂炎。這樣就不能照常使用number變量了。調(diào)用release之后设哗,已經(jīng)無法保證所指的對象仍然存活唱捣。當(dāng)然,根據(jù)本例中的代碼网梢,我們顯然知道number對象在調(diào)用了release之后仍然存活爷光,因為數(shù)組還在引用著它。然而絕不應(yīng)該假設(shè)此對象一定存活澎粟,也就是說蛀序,不要像下面這樣編寫代碼:
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
NSLog(@"number = %@", number);
即便上述代碼在本例中可以正常執(zhí)行,也仍然不是個好辦法活烙。如果調(diào)用release之后徐裸,基于某些原因,其保留計數(shù)降至0啸盏,那么number對象所占內(nèi)存也許會回收重贺,這樣的話,再調(diào)用NSLog可能就將使程序崩潰了回懦。筆者在這里只說"可能"气笙,而沒說"一定",因為對象所占的內(nèi)存在"解除分配"(deallocated)之后怯晕,只是放回"可用內(nèi)存池"(avaiable pool)潜圃。如果執(zhí)行NSLog時尚未覆寫對象內(nèi)存,那么該對象仍然有效舟茶,這時程序不會崩潰谭期。由此可見: 因過早釋放對象而導(dǎo)致的bug很難調(diào)試堵第。
為避免不經(jīng)意間使用了無效對象,一般調(diào)用完release之后都會清空指針隧出。這就能保證不會出現(xiàn)可能指向無效對象的指針踏志,這種指針通常稱為"懸掛指針"(dangling pointer)。比方說胀瞪,可以這樣編寫代碼來防止此情況發(fā)生:
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
number = nil;
屬性存取方法中的內(nèi)存管理
如前所述针余,對象圖由互相關(guān)聯(lián)的對象所構(gòu)成。剛才那個例子中的數(shù)組通過在其元素上調(diào)用retain方法來保留那些對象凄诞。不光是數(shù)組圆雁,其他對象也可以保留別的對象,這一般通過訪問"屬性"(參見第6條)來實現(xiàn)幔摸,而訪問屬性時摸柄,會用到相關(guān)實例變量的獲取方法及設(shè)置方法。若屬性為"strong關(guān)系"(strong relationship)既忆,則設(shè)置的屬性值會保留驱负。比方說,有個名叫foo的屬性由名叫_foo的實例變量所實現(xiàn)患雇,那么跃脊,該屬性的設(shè)置方法會是這樣:
- (void)setFoo:(id)foo {
[foo retain];
[_foo release];
_foo = foo;
}
此方法將保留新值并釋放舊值,然后更新實例變量苛吱,令其指向新值酪术。順序很重要。假如還未保留新值就先把舊值釋放了翠储,而且兩個值又指向同一個對象绘雁,那么,先執(zhí)行的release操作就可能導(dǎo)致系統(tǒng)將此對象永久回收援所。而后續(xù)的retain操作則無法令這個已經(jīng)徹底回收的對象復(fù)生庐舟,于是實例變量就成了懸掛指針。
自動釋放池
在Objective-C的引用計數(shù)架構(gòu)中住拭,自動釋放池是一項重要特性挪略。調(diào)用release會立刻遞減對象的保留計數(shù)(而且還有可能令系統(tǒng)回收此對象),然而有時候可以不調(diào)用它滔岳,改為調(diào)用autorelease杠娱,此方法會在稍后遞減計數(shù),通常是在下一次"事件循環(huán)"(event loop)時遞減谱煤,不過也可能執(zhí)行得更早些(參見第34條)摊求。
此特性很有用,尤其是在方法中返回對象中返回對象時更應(yīng)該用它趴俘。在這種情況下睹簇,我們并不總是想令方法調(diào)用者手工保留其值奏赘。比方說寥闪,有下面這個方法:
- (NSString*)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
return str;
}
此時返回的str對象其保留計數(shù)比期望值要多1(+1 retain count)太惠,因為調(diào)用alloc會令保留計數(shù)加1,而又沒有與之對應(yīng)的釋放操作疲憋。保留計數(shù)多1凿渊,就意味著調(diào)用者要負(fù)責(zé)處理多出來的這一次保留操作。必須設(shè)法將其抵消缚柳。這并不是說保留計數(shù)本身就一定是1埃脏,它可能大于1,不過那取決于"initWithFormat:"方法內(nèi)的實現(xiàn)細(xì)節(jié)秋忙。你要考慮的是如何將多出來的這一次保留操作抵消掉彩掐。
但是,不能在方法內(nèi)釋放str灰追,否則還沒等方法返回堵幽,系統(tǒng)就把該對象回收了。這里應(yīng)該用autorelease弹澎,它會在稍后釋放對象朴下,從而給調(diào)用者留下了足夠長的時間,使其可以在需要時先保留返回值苦蒿。換句話說殴胧,此方法可以保證對象在跨越"方法調(diào)用邊界"(method call boundary)后一定存活。實際上佩迟,釋放操作會在清空最外層的自動釋放池(參見第34條)時執(zhí)行团滥,除非你有自己的自動釋放池,否則這個時機指的就是當(dāng)前線程的下一次事件循環(huán)报强。改寫stringValue方法灸姊,使用autorelease來釋放對象:
- (NSString*)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
return [str autorelease];
}
修改之后,stringValue方法把NSString對象返回給調(diào)用者時躺涝,此對象必然存活厨钻。所以我們能夠像下面這樣使用它:
NSString *str = [self stringValue];
NSLog(@"The string is: %@", str);
由于返回的str對象將于稍后自動釋放,所以多出來的那一次保留操作到時自然就會抵消坚嗜,無須再執(zhí)行內(nèi)存管理操作夯膀。因為自動釋放池中的釋放操作要等到下一次事件時才會執(zhí)行,所以NSLog語句在使用str對象前不需要手工執(zhí)行保留操作苍蔬。但是诱建,假如要持有此對象的話(比如將其設(shè)置給實例變量),那就需要保留碟绑,并于稍后釋放:
_instanceVariable = [[self stringValue] retain];
// …
[_instanceVariable release];
由此可見俺猿,autorelease能延長對象生命期茎匠,使其在跨越方法調(diào)用邊界后依然可以存活一段時間。
保留環(huán)
使用引用計數(shù)機制時押袍,經(jīng)常要注意的一個問題就是"保留環(huán)"(retain cycle)诵冒,也就是呈環(huán)狀相互引用的多個對象。這將導(dǎo)致內(nèi)存泄漏谊惭,因為循環(huán)中的對象其保留計數(shù)不會降為0.對于循環(huán)中的每個對象來說汽馋,至少還有另外一個對象引用著它。下圖里的每個對象都引用了另外兩個對象之中的一個圈盔。在這個循環(huán)里豹芯,所有對象的保留計數(shù)都是1。
在垃圾收集環(huán)境中驱敲,通常將這種情況認(rèn)定為"孤島"(island of isolation)铁蹈。此時,垃圾收集器會把三個對象全都回收走众眨。而在Objective-C的引用計數(shù)架構(gòu)中握牧,則享受不到這一便利。通常采用"弱引用"(weak reference围辙, 參見第33條)來解決此問題我碟,或是從外界命令循環(huán)中的某個對象不再保留另外一個對象。這兩種辦法都能打破保留環(huán)姚建,從而避免內(nèi)存泄漏矫俺。
要點
- 引用計數(shù)機制通過可以遞增遞減的計數(shù)器來管理內(nèi)存。對象創(chuàng)建好之后掸冤,其保留計數(shù)至少為1.若保留計數(shù)為正厘托,則對象繼續(xù)存活。當(dāng)保留計數(shù)降為0時稿湿,對象就被銷毀了铅匹。
- 在對象生命期中,其余對象通過引用來保留或釋放此對象饺藤。保留與釋放操作分別會遞增及遞減保留計數(shù)包斑。