標籤:
導言
Objective-C語言使用引用計數來管理記憶體,也就是說,每個對象都有個可以遞增或遞減的計數器。如果想使某個對象繼續存活,那就遞增其引用計數;用完了之後,就遞減其計數。計數為0,就表示沒人關注此對象了,於是,就可以把它銷毀。
從Mac OS X 10.8開始,“垃圾收集器”(garbage collector)已經正式廢棄了,以Objective-C代碼編寫Mac OS X程式時不應再使用它,而iOS則從未支援過垃圾收集。因此,掌握引用計數機制對於學好Objective-C來說十分重要。Mac OS X程式已經不能再依賴垃圾收集器了,而iOS系統不支援此功能,將來也不會支援。
已經用過ARC的人可能會知道:所有與引用計數有關的方法都無法編譯,然而現在先暫時忘掉這件事。那些方法確實無法用在ARC中,不過本文就是要從Objective-C的角度講解引用計數,而ARC實際上也是一種引用計數機制,所以,還是要談談這些在開啟ARC功能時不能直接調用的方法。
工作原理
在引用計數架構下,對象有個計數器,用以表示當前有多少個事物想令此對象繼續存活下去。這在Objective-C中叫做“保留計數”(retain count),不過也可以叫“引用計數”(reference count)。NSObject協議聲明了下面三個方法用於操作計數器,以遞增或遞減其值:
1)retain 遞增保留計數。
2)release 遞減保留計數。
3)autorelease 待稍後清理“自動釋放池”(autorelease pool)時,再遞減保留計數。
是對象建立及保留計數操作的。
對象圖中,ObjectB與ObjectC都引用了ObjectA。若ObjectB與ObjectC都不再使用ObjectA,則其保留計數降為0,於是便可摧毀了。還有其他對象想令ObjectB與ObjectC繼續存活,而應用程式裡又有另外一些對象想令那些對象繼續存活。如果按“引用樹”回溯,那麼最終會發現一個“根對象”(root object)。在Mac OS X應用程式中,此對象是NSApplication對象;而在iOS應用程式中,則是UIApplication對象。兩者都是應用程式啟動時建立的單例。
下面這段代碼有助於理解這些方法的用法:
NSMutableArray *array = [[NSMutableArray alloc] init];NSNumber *number = [[NSNumber alloc] initWithInt:1337];[array addObject:number];[number release];//do something with ‘array‘[array release];
由於代碼中直接調用了release方法,所以在ARC下無法編譯。在Objective-C中,調用alloc方法所返回的對象由調用者所擁有。也就是說,調用者已通過alloc方法表達了想令該對象繼續存活下去的意願。不過,這並不是說對象此時的保留計數就是1。在alloc或“initWithInt:”方法的代碼實現中,也許還有其他對象也保留了此對象。絕不能說保留計數一定是某個值,只能說你所執行的操作的遞增了該計數還是遞減了該計數。
建立完數組後,把number對象加入其中。調用數組的“addObject:”方法時,數組也會在number上調用retain方法,以期繼續保留此對象。這時,保留計數至少為2。接下來,代碼不再需要number對象了,於是將其釋放。現在的保留計數至少為1。這樣就不能照常使用number變數了。調用release之後,已經無法保證所指的對象仍然存活。當然,根據本例中的代碼,我們顯然知道number對象在調用了release之後仍然存活,因為數組還在引用著它。然而絕不應該假設此對象一定存活,也就是說,不要像下面這樣子編寫代碼:
NSNumber *number = [[NSNumber alloc] initWithInt:1337];[array addObject:number];[number release];NSLog(@"number = %@", number);
即便上述代碼在本例中可以正常執行,也仍然不是個好辦法。如果調用release之後,基於某些原因,其保留計數降至為0,那麼number對象所佔記憶體也許會回收,這樣的話,再調用NSLog可能就將使程式崩潰了。為什麼是“可能”,因為對象所佔的記憶體在“解除配置”(deallocated)之後,只是放回“可用記憶體池”(avaiable pool)。如果執行NSLog時還尚未覆寫對象記憶體,那麼該對象仍然有效,這是程式不會崩潰。故,因過早釋放對象而導致的bug很難調試。
為避免在不經意間使用了無效對象,一般調用完release之後都會清null 指標。這就能保證不會出現可能指向無效對象的指標,這種指標通常稱為“懸掛指標”(dangling pointer)。例如,可以這樣編寫代碼來防止此情況發生:
NSNumber *number = [[NSNumber alloc] initWithInt:1337];[array addObject:number];[number release];number = nil;
屬性存取方法中的記憶體管理
如前所述,對象圖由相互關聯的對象所構成。剛才那個例子中的數組通過在其元素上調用retain方法來保留那些對象。不光數組,其他對象也可以保留別的對象,這一般通過訪問“屬性”來實現,而訪問屬性時,會用到相關執行個體變數的擷取方法和設定方法。若屬性為“strong關係”(strong relationship),則設定的屬性值會保留。比方說,有個名叫foo的屬性由名為_foo的執行個體變數所實現,那麼,該屬性的設定方法會是這樣:
-(void)setFoo:(id)foo { [foo retain]; [_foo release]; _foo = foo;}
此方法將保留新值並釋放舊值,然後更新執行個體變數,令其指向新值。順序很重要。假如還未保留新值就先把舊值釋放了,而兩個值又指向同一個對象,那麼,先執行release操作就可能導致系統將此對象永久回收。而後續的retain操作則無法令這個已經徹底回收的對象複生,於是執行個體變數就成了懸掛指標。
自動釋放池
在Objective-C的引用計數架構中,自動釋放池是一項重要特性。調用release會立刻遞減對象的保留計數(而且還可能令系統回收此對象),然而有時候可以不調用它,改為調用autorelease,此方法會在稍後遞減計數,通常是在下一次“事件迴圈”(event loop)時遞減,不過也可能執行得更早些。
此特性很有用,尤其是在方法中返回對象時更應該用它。在這種情況下,我們並總是想令方法調用者手工保留其值。比方說,有下面這個方法:
-(NSString *)stringValue { NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self]; return str;}
此時返回的str對象其保留計數比期望值要多1,因為調用者alloc會令保留計數加1,而又沒有與之對應的釋放操作。保留計數多1,就意味著調用者要負責處理多出來的這一次保留操作。必須設法將其抵消。這並不是說保留計數本身就一定是1,它可能大於1,不過那取決於“initWithFormat:”方法內的實現細節。你要考慮的是如何將多出來的這一次保留操作抵消掉。但是,不能在方法呢你釋放str,否則還沒等方法返回,系統就把該對象回收了。這裡應該用autorelease,它會在稍後釋放對象,從而給調用者留下了足夠長的時間,使其可以在需要時先保留傳回值。換句話說,此方法可以保證對象在跨越“方法調用邊界”(method call boundary)後一定存活。實際上,釋放操作會在清空最外層的自動釋放池時執行,除非你有自己的自動釋放池,否則這個時機指的就是當前線程的下一次事件迴圈。改寫stringValue方法,使用autorelease來釋放對象:
-(NSString *)stringValue { NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self]; return [str autorelease];}
修改之後,stringValue方法把NSString對象返回給調用者,此對象必然存活。所以我們能夠如此使用它:
NSString *str = [self stringValue];NSLog(@"The string is: %@", str);
由於返回的str對象將於稍後自動釋放,所以多出來的那一次保留操作時自然就會抵消,無須再執行記憶體管理操作。因為自動釋放池中的釋放操作要等到下一次事件迴圈時才會執行,所以NSLog語句在使用str對象前不需要手工執行保留操作。但是,假如要持有此對象的話(比如將其設定給執行個體變數),那就需要保留,並於稍後釋放:
_instanceVariable = [[self stringValue] retain];//...[_instaceVariable release];
由此可見,autorelease能延長對象生命期,使其在跨越方法調用邊界後依然可以存活一段時間。
保留環
使用引用計數機制時,經常要注意的一個問題就是“保留環”(retain cycle),也就是呈環狀相互引用的多個對象。這將導致記憶體泄露,因為迴圈中的對象其保留計數不會降為0。對於迴圈中的每個對象來說,至少還有另外一個對象引用著它。
如,在這個迴圈裡,所以對象的保留計數都是1。在垃圾收集環境中,通常將這種情況認定為“孤島”(island of isolation)。此時,垃圾收集器會把三個對象全部回收。而在Objective-C的引用計數架構中,則享受不到這一便利。通常採用“弱引用”(weak reference)來解決此問題,或是從外界命令迴圈中的某個對象不再保留另外一個對象。這兩種辦法都能打破保留環,從而避免記憶體泄露。
小結
引用計數機制通過可以遞增遞減的計數器來管理記憶體。對象建立好之後,其保留計數至少為1。若保留計數為正,則對象繼續存活。當保留計數降為0時,對象就被銷毀。
在對象生命週期中,其餘對象通過引用來保留或釋放此對象。保留與釋放操作分別會遞增及遞減保留計數。
Objective-C中的引用計數