Objective-C中的引用計數

 導言

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功能時不能直接調用的方法。oop

工做原理

在引用計數架構下,對象有個計數器,用以表示當前有多少個事物想令此對象繼續存活下去。這在Objective-C中叫作「保留計數」(retain count),不過也能夠叫「引用計數」(reference count)。NSObject協議聲明瞭下面三個方法用於操做計數器,以遞增或遞減其值:this

1)retain 遞增保留計數。spa

2)release 遞減保留計數。線程

3)autorelease 待稍後清理「自動釋放池」(autorelease pool)時,再遞減保留計數。指針

 

上圖是對象建立及保留計數操做的效果圖。調試

 

上圖對象圖中,ObjectB與ObjectC都引用了ObjectA。若ObjectB與ObjectC都再也不使用ObjectA,則其保留計數降爲0,因而即可摧毀了。還有其餘對象想令ObjectB與ObjectC繼續存活,而應用程序裏又有另一些對象想令那些對象繼續存活。若是按「引用樹」回溯,那麼最終會發現一個「根對象」(root object)。在Mac OS X應用程序中,此對象是NSApplication對象;而在iOS應用程序中,則是UIApplication對象。二者都是應用程序啓動時建立的單例。code

下面這段代碼有助於理解這些方法的用法:orm

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以後都會清空指針。這就能保證不會出現可能指向無效對象的指針,這種指針一般稱爲「懸掛指針」(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時,對象就被銷燬。

在對象生命週期中,其他對象經過引用來保留或釋放此對象。保留與釋放操做分別會遞增及遞減保留計數。

 

參考書籍:《Effective Objective-C 2.0》

相關文章
相關標籤/搜索