29.理解引用計數
30.以ARC簡化引用計數
31.在dealloc方法中只釋放引用並解除監聽
32.編寫異常安全代碼時留意內存管理問題
Objective-C語言使用引用計數來管理內存,也就是說每個對象都有個可以遞增或遞減的計數器。如果想使某個對象繼續存活,那就遞增其引用計數;用完之後,就遞減其計數。計數變為0,就表示無人關注此對象了,於是,就可以把它銷毀。
在引用計數架構下,對象有個計數器,用以表示當前有多少個事物想令此對象繼續存活下去。這在Objective-C中叫做保留計數,也叫引用計數。NSObject協議聲明了下面3個方法用於操作計數器,以遞增或遞減其值(ARC中不可用):
retain // 遞增保留計數 release // 遞減保留計數 autorelease // 待稍後清理自動釋放池時再遞減保留計數
對象創建出來時,其保留計數至少為1.若想令其繼續存活,則調用retain方法。要是某部分代碼不再使用此對象,那就調用release或autorelease方法。最終保留計數歸0時,對象就回收了。
為了避免在不經意間使用了無效對象,一般調用完release之後都會清空指針。這就能保證不會出現可能指向無效對象的指針,這種指針通常被稱為懸掛指針。
NSMutableArray *array = [[NSMutableArray alloc] init]; // number創建,保留計數為1 NSNumber *number = [[NSNumber alloc] initWithInt:1337]; // 數組引用了number,number保留計數為2 [array addObject:number]; // 釋放number,保留計數為1 [number release]; // 調用release後,無法保證number仍然存活(雖然在本例中可以明顯看到number還存活),應當清空指針。 number = nil;
屬性為strong時,屬性的setter方法是先保留新值再釋放舊值,然後更新實例變量,使其指向新值。
- (void)setFoo:(id)foo{ [foo retain]; [_foo release]; _foo = foo; }
執行順序呢很重要,假如還未保留新值就先把舊值釋放了,而且兩個值又指向同一個對象,那麼先執行的release操作就可能導致系統將此對象永久回收。而後續的retain操作則無法令這個已徹底回收的對象復生,實例變量就成為懸掛指針。
調用release會立刻遞減對象的保留計數(可能會令系統回收此對象),然而有時候可以不調用它,改為調用autorelease,此方法會在稍後遞減計數。(通常是下一次時間循環時遞減)
- (NSString*)stringValue{ NSString *str = [[NSString alloc] initWithFormat:@"I am this %@",self]; return str; }
此時返回的str對象其保留計數比期望值要多1,因為調用alloc會令保留計數加1,而又沒有與之對應的釋放操作。也不能在方法內釋放str,否則還沒等方法返回,系統就把對象回收了。這裡應該使用autorelease,它會在稍後釋放對象。
- (NSString*)stringValue{ NSString *str = [[NSString alloc] initWithFormat:@"I am this %@",self]; return [str autorelease]; }
使用ARC時,引用計數實際上還是在執行。只不過保留與釋放操作現在是由ARC自動添加。因此直接在ARC下調用retain、release、autorelease、dealloc這些內存管理方法是非法的。
實際上,ARC在調用這些方法時,直接調用的是其底層C語言版本,如ARC會調用與retain等價的底層函數objc_retain,這樣做性能更好,這也是不能重寫retain、release、autorelease的原因。
ARC確立了方法名的硬性規定。若方法名以alloc、new、copy、mutableCopy開頭,其返回對象歸調用者所有。其他方法返回的對象並不歸調用者所有。
+ (EOCPerson*)newPerson{ EOCPerson *person = [[EOCPerson alloc] init]; return person; // 方法以new開頭,返回值歸調用者所有,釋放由調用者負責,ARC不會在這裡自動添加語句 } + (EOCPerson*)somePerson{ EOCPerson *person = [[EOCPerson alloc] init]; return person; // 返回值不歸調用者所有,ARC會自動將return person替換為等價return [person autorelease]的語句 } - (void)doSomething{ EOCPerson *person1 = [EPCPerson newPerson]; EOCPerson *person2 = [EPCPerson somePerson]; // ARC會自動添加等價[person1 release]的語句(person1是由這塊代碼所有) }
在應用程序中,可用下列修飾符來改變局部變量與實例變量的語義:
__strong // 默認語義,保留此值 __unsafe_unreatined // 不保留此值,可能不安全,出現懸掛指針 __weak // 不保留此值,安全,系統回收對象時會清空對象 __autorelease // 把對象按引用傳遞給方法時,使用該修飾符。此值在方法返回時自動釋放。
在手動管理引用計數時,可能會像下面這樣來編寫dealloc方法清空實例變量
- (void)dealloc{ [_foo release]; [_bar release]; [super dealloc]; }
用了ARC之後,就不需要再編寫這樣的dealloc方法了,ARC會自動清理內存。不過,如果有非Objective-C對像(如CoreFoundation)中的對象或是由malloc()分配在堆中的內存,仍然需要清理。在ARC中不能直接調用dealloc,但是可以重寫dealloc方法,ARC會自動運行此方法,並調用其中超類的dealloc方法。
- (void)dealloc{ // 釋放非Objective-C對象 CFRelease(_coreFoundationObject); // 釋放malloc()分配的堆內存 free(_heapAllocatedMemoryBlob); }
對象在經歷其生命期後,最終會為系統所回收,這時就要執行dealloc方法了。在每個對象的生命周期內,此方法僅執行一次,也就是當保留計數降為0的時候。dealloc方法會由運行期系統調用,開發者不能自己調用。
ARC會自動釋放所有Objective-C對象,dealloc中需要手動釋放非Objective-C對象,除此之外,還需要把原來配置過的觀測行為都清理掉。如果用NSNotificationCenter給此對象注冊過某種通知,那麼一般應該在這裡注銷。不然通知系統可能會把通知發送給已回收的對象,引起系統崩潰。
- (void)dealloc{ CFRelease(_coreFoundationObject); // 注銷通知 [[NSNotificationCenter defaultCenter] removeObserver:self]; }
雖說應該於dealloc中釋放引用,但是開銷較大或系統內稀缺資源不在此列。如文件描述符、套接字、大塊內存,不然會導致保留稀缺資源時間過長。通常應該實現另一個方法,但應用程序用完資源後就調用此方法清理資源。再在dealloc中進行檢查,防止開發者忘記清理資源。
// 清理資源的方法 - (void)close{ /* 清理資源 */ _closed = YES; } - (void)dealloc{ // 如果忘記清理資源,則輸出錯誤日志並清理資源 if(!_closed){ NSLog(@"ERROR:close was not called before dealloc!"); [self close]; } }
本例中,在dealloc調用了其它方法,不過是為了偵測編程錯誤而破例。正常情況下,不要在dealloc中隨便調用其他方法。因為對象已經處於正在回收狀態,如果在這裡調用的方法又要異步執行某些任務,等任務結束後這個對象可能已經被徹底摧毀了,導致程序崩潰。
dealloc裡也不要調用屬性的存取方法,屬性可能正處於KVO機制的監控之下,屬性的觀察者可能會在屬性值改變時保留或使用這個即將回收的對象,導致錯誤。
Objective-C的錯誤模型表明,異常只應在發生嚴重錯誤後拋出,不過有時候仍然需要編寫代碼來捕獲並處理異常。
使用手動計數時:
@try{ EOCSomeClass *object = [[EOCSomeClass alloc] init]; [object doSomethingThatMayThrow]; } @catch(...){ Nslog(@"There was an error!"); } // 無論是否發生異常,@finally塊中的代碼都會執行 @finally{ [object release]; }
使用@finally塊可以在發生異常時也能釋放對象。
而ARC環境下,不能調用release,無法像手動管理那樣將釋放操作移到@finally塊中。ARC又不會自動處理,這種情況下可以打開編譯器的-fobjc-arc-exceptions標志來開啟ARC生成安全處理異常所用的附加代碼。只是這段代碼會嚴重影響運行期的性能,即使不拋出異常。
所以一般來說,只有當應用程序必須因異常情況而終止時才應拋出異常,這時候應用程序即將終止,是否發生內存洩漏已經無關緊要了,因此不用添加安全處理異常所用的附加代碼了。如果有大量異常捕獲操作時,應考慮重構代碼,用21條的NSError式錯誤信息傳遞法來取代異常。