【Effective Objective-C 2.0讀書筆記】第五章:內存管理

Objective-C語言以引用計數來管理內存,這令許多初學者糾結,要是用過以“垃圾收集器”(garbage collector)來管理內存的語言,那麼更會如此。“自動引用計數”機制緩解了此問題,不過使用時有很多重要的注意事項,以確保對象模型正確,不致內存泄漏。本章提醒讀者注意內存管理中易犯的錯誤。

第29條:理解引用計數

從Mac OS X 10.8開始,垃圾收集器已經正式廢棄,而iOS則從未支持過垃圾收集。因此,掌握引用計數機制對於學好Objective-C十分重要。

引用計數工作原理

在引用計數架構下,對象有個計數器,表示當前有多少對象想令該對象繼續存活,在Objective-C中叫做“保留計數”(retain count)或“引用計數”(reference count)。NSObject協議聲明瞭三個方法用於操作計數器,以遞增/遞減其值:
retain:遞增保留計數。
release:遞減保留計數。
autorelease:待稍後清理自動釋放池(autorelease pool)時,再遞減保留計數。

查看保留計數的方法是retainCount,此方法不太有用,即便在調試時也是如此,故筆者(與蘋果公司)並不推薦使用這個方法。

爲避免在不經意間使用了無效對象,一般調用完release之後都會清空指針。這就能確保不會出現可能指向無效對象的指針,這種指針叫做“懸掛指針”(dangling pointer)。例如,可以這樣編寫代碼以防止此情況發生:

NSNumber *number = [[NSNumber alloc] initWithInt: 1337];
[array addObject:number];
[number release];
number = nil;

屬性存取方法中的內存管理

對象圖是由相互關聯的對象所構成。不光是數組,其他對象也可以保留別的對象,這一般通過訪問屬性來實現。訪問屬性時,會用到相關實例變量的獲取方法及設置方法。若屬性爲“strong關係”(strong relationship),則設置的屬性值會保留。例如:

- (void)setFoo:(id)foo{
	[foo retain];
	[_foo release];
	_foo = foo;
}

在以上代碼中,請注意先保留新值再釋放舊值的順序。

本人註解:

需要特別注意的是:
如果通過self->_foo的方式引用實例變量,當self爲nil時,會導致EXC_BAD_ACCESS崩潰錯誤。
原因是:此時是直接訪問指針變量指向的內存地址去取值,而該對象已經被釋放。
建議都採用self.foo這種通過屬性訪問的方式,這樣當self爲nil時,相當於是向一個nil對象發送消息,不會引發crash;在init和dealloc方法中例外,可通過_foo來讀寫值。

自動釋放池

autorelease方法能夠延長對象生命期,使其在跨越方法調用邊界(method call boundary)後依然可以存活一段時間。它會在稍後遞減保留計數,通常是在下一次“事件循環”(event loop)時遞減,不過也可以通過@autoreleasepool讓這個過程執行得更早。

保留環

使用引用計數機制時,經常需要注意的一個問題就是“保留環”(retain cycle),也就是呈環狀相互引用的多個對象。這將導致內存泄漏。

在垃圾收集環境中,通常將這種情況認定爲“孤島”(island of isolation)。此時垃圾收集器會把這三個對象全部回收走。而在Objective-C中,由於沒有垃圾收集器,通常採用“弱引用”(weak reference)來解決此問題,或是從外界命令引用循環中的某個對象不再保留另外一個對象來打破這個保留環,從而避免內存泄漏。

第30條:以ARC簡化引用計數

引用計數這個概念相當容易理解,需要執行保留及釋放操作的地方也很容易就能看出來。故Clang編譯器項目帶有一個“靜態分析器”(static analyzer),用於指明程序裏引用計數出問題的地方。靜態分析器還有更爲深入的用途,可以根據需要預先加入
恰當的保留或釋放操作以避免這些問題,即自動引用計數。

由於ARC會自動執行retainreleaseautorelease等操作,故直接在ARC下調用這些內存管理方法是非法的。具體來說,不能調用下列方法:retainreleaseautoreleasedealloc。直接調用上述任何方法都會產生編譯錯誤,因爲ARC要分析何處應該自動調用內存管理方法。

實際上,ARC在調用這些方法時,並不通過普通的Objective-C消息派發機制,而是直接調用其底層的C語言版本,以提升性能。例如,ARC會調用與retain等價的底層函數objc_retain。這也是不能覆寫retainreleaseautorelease的緣由,因爲這些方法在ARC下從來不會被直接調用。

*使用ARC時必須遵循的方法命名規則

將內存管理語義在方法名種表示出來早已是Objective-C的慣例,而ARC則將之確立爲硬性規定。若方法名以alloc,new,copy,mutableCopy這些詞語開頭,則其返回的對象歸調用者所有,即調用這些方法的那段代碼要負責釋放返回的對象。

若方法名不以上述四個詞語開頭,則表示其返回的對象並不歸調用者所有。在這種情況下,返回的對象會自動釋放,所以其值在跨越方法調用邊界後依然有效。要想使對象多存活一段時間,必須令調用者保留它才行。

除了會自動調用“保留”與“釋放”方法之外,使用ARC還有其他好處,可以執行一些手動操作很難甚至無法完成的優化。例如,在編譯期,ARC會把能夠相互抵消的retain、release、autorelease操作約簡。這也是之所以建議大家在以後編碼中都應該用ARC的原因。

例如autorelease及緊跟其後的retain操作,爲了優化代碼,在方法中返回自動釋放的對象時,要執行一個特殊函數objc_autoreleaseReturnValue,而不直接調用對象的autorelease方法,此函數會檢查當前方法返回後即將執行的那段代碼。若發現將要執行retain操作,則設置全局數據結構(因處理器而異)中的一個標誌位,而不執行autorelease操作;之後也不直接執行retain,而是改爲執行objc_retainAutoreleasedReturnValue函數。此函數檢查上述提到的標誌位,若已經置位則不執行retain操作。設置並檢測標誌位,要比調用autorelease和retain更快。

變量的內存管理語義

在應用程序中,可用下列修飾符來改變局部變量與實例變量的語義:

  • __strong:默認語義,保留其值。

  • __unsafe_unretained:不保留其值,這麼做可能不安全,因爲等到再次使用該變量時,對象可能已經被回收了。

  • __weak:不保留其值,但是變量可以安全使用,因爲如果對象被系統回收了,變量也會自動清空。需要注意的是,只有使用新版(Mac OS X 10.7、iOS 5.0及其後續版本)的運行期程序庫,加了__weak修飾符的weak引用纔會自動清空,因爲實現這個功能要借用到新版所添加的一些功能。

  • __autoreleasing:把對象“按引用傳遞”給方法時,使用這個特殊的修飾符,其值在方法返回時自動釋放。

ARC如何清理實例變量

凡是強引用的變量都必須釋放,ARC會在自動生成的dealloc方法中插入這些代碼。

回收Objective-C++對象時,待回收的對象會調用所有C++對象的析構函數。編譯器如果發現某個對象裏含有C++對象,就會生成名爲.cxx_destruct的方法。ARC藉助此特性,在.cxx_destruct方法中生成清理內存代碼。

不過,如果有非Objective-C的對象,比例CoreFoundation中的對象(需要調用CFRetain/CFRelease)或是由malloc分配在堆中的內存,仍然需要在dealloc方法中顯式清理。然而不需要像原來那樣調用超類的dealloc方法,因爲ARC會自動在.cxx_destruct方法中調用超類的dealloc方法。

覆寫內存管理方法

不使用ARC時,可以覆寫內存管理方法。比如,在實現單例類時,因爲單例不可釋放,所以我們經常覆寫release方法,將其替換爲“空操作”(no-op)。

但在ARC環境下不能這樣做,會干擾到ARC分析對象生命期的工作。而且,由於開發者不可調用及覆寫這些方法,所以ARC能夠優化retain、release、autorelease操作,使之不經過Objective-C的消息派發機制。優化後的操作,直接調用隱藏在運行期程序庫中的C函數。

第31條:在dealloc方法中只釋放引用並解除監聽

對象在經歷其生命期後,最終爲系統所回收,這時運行期系統就要執行dealloc方法了。具體何時執行則無法保證,而且你決不應該自己調用dealloc方法。一旦運行期系統調用dealloc方法後,對象就不再有效了。

在dealloc方法裏,應該做的事情就是釋放指向其他對象的引用,並取消原來訂閱的“鍵值觀測”(KVO)或NSNotificationCenter等通知,不要做其他事情。

如果對象持有文件描述符等系統資源,應該專門編寫一個方法來釋放此資源,例如稱作close方法,而且在用完資源後必須調用close方法。

執行異步任務的方法不應在dealloc裏調用;只能在正常狀態下執行的那些方法也不應在dealloc方法裏調用,因爲此時對象已處於正在回收的狀態了。

需要注意的是,在dealloc方法裏不要調用屬性的存取方法,因爲有人可能會覆寫這些方法,並於其中做一些無法在回收階段安全執行的操作。此外,屬性可能正處於鍵值觀測機制的監控下,該屬性的觀察者可能會在屬性改變時保留或使用這個即將回收的對象。這會令運行期系統的狀態完全失調,導致莫名其妙的錯誤。

第32條:編寫“異常安全代碼”時留意內存管理問題

純C中沒有“異常”(exception),而C++與Objective-C都支持“異常”。實際上,在當前的運行期系統中,C++與Objective-C的異常相互見人。也就是說,從其中一門語言裏拋出的異常能用另一種語言所編的“異常處理程序”(exception handler)來捕獲。

Objective-C的錯誤模型表明,異常只應在發生嚴重錯誤後拋出。雖說如此,有時仍然需要捕獲並處理異常,例如使用Objective-C++來編碼時,或編碼中用到了第三方程序庫而此程序庫所拋出的異常又不受你控制。此外,有些系統庫也會用到異常。

捕獲異常時,一定要注意將try塊內所創建的對象都清理乾淨。

在默認情況下,ARC不生成安全處理異常所需的清理代碼。開啓-fobc-arc-exceptions編譯器標誌後,可生成這種代碼,不過會導致應用程序變大,而且會降低運行效率。但最重要的是:在發生大量異常捕獲操作時,應考慮重構代碼,用NSError式錯誤信息傳遞法來取代異常。

有種情況下編譯器會自動開啓-fobc-arc-exceptions編譯器標誌,就是處於Objective-C++模式時。因爲C++處理異常的代碼與ARC實現的附加代碼類似,故令ARC加入安全處理異常的代碼,其性能損失不會太大。由於C++頻繁使用異常,故Objective-C++程序員很可能也會使用異常。

第33條:以弱引用避免保留環

對象圖裏經常出現一種情況,即幾個對象以某種方式相互引用,從而形成“環”。由於Objective-C內存管理模型採用引用計數架構,所以這種情況就會導致內存泄漏。保留環裏的對象無法爲外界所訪問,但這些對象之間尚有引用,使得它們都能繼續存活下去,而不會爲系統所回收。

避免保留環的最佳方式是弱引用。這種引用常用來表示“非擁有關係”(non-owning relationship)。將屬性聲明爲unsafe-unretained或者weak即可。unsafe_unretained表明屬性可能不安全,而且不歸此實例所擁有,語義與assign等價,但前者多用於修飾對象類型,後者通常只用於基本類型(integral types),例如int、float、結構體等。weak屬性特質與unsafe_unretained作用完全相同,只是當系統將屬性回收後,屬性值就自動設爲nil。使用weak比unsafe_unretained可以令代碼更安全。不過無論如何,只要在所指對象已經徹底銷燬後還繼續使用弱引用,那就依然是個bug。

第34條:以“自動釋放池”降低內存峯值

Objective-C對象的生命期取決於其引用計數。釋放對象有兩種方式:一種是release方法,使其保留計數立即遞減;另一種是autorelease方法,將其加入自動釋放池中。自動釋放池用於存放那些需要在稍後某個時刻釋放的對象。清空(drain)自動釋放池時,系統會向其中的對象發生release消息。

Mac OS X/iOS系統會自動創建一些線程,比如主線程或GCD機制中的線程,這些線程默認都有自動釋放池,每次執行事件循環時就會將其清空。通常只有一個地方需要創建自動釋放池,即main函數裏,用來包裹應用程序主入口點(main application entry point):

int main(int argc, char *argv[]) {
	@autoreleasepool {
		return UIApplicationMain(argc, 
					argv,
					nil,
					@"EOCAppDelegate");
	}
}

自動釋放池於{處創建,並於對應的}處自動清空。位於自動釋放池範圍內的對象,將在此範圍末尾處收到release消息。自動釋放池可以嵌套。

可以利用自動釋放池來降低應用程序在運行過程中的內存峯值。加入自動釋放池塊後,系統會在塊的末尾把它包裹的對象回收掉。例如:

NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) {
	@autoreleasepool {
		EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
		[people addObject:person];
	}
}

自動釋放池機制就像棧(stack)一樣。系統創建好自動釋放池之後,就將其推入棧中,而清空自動釋放池則相當於將其從棧中彈出。在對象上執行自動釋放池操作,就等於將其放入棧頂的那個池裏。

在ARC出現之間,自動釋放池有種老式寫法,即使用NSAutoreleasePool對象。然而@autoreleasepool這種寫法能創建出更爲輕便的自動釋放池。

第35條:用“殭屍對象”調試內存管理問題

啓用Cocoa提供的殭屍對象調試功能之後,運行期系統會把所有已經回收的實例轉化成特殊的“殭屍對象”,而不會真正回收它們。這種對象所在的核心內存無法重用,因此不可能遭到覆寫。殭屍對象收到消息後,會拋出異常,其中準確說明了發送過來的消息,並描述了回收之前的那個對象。殭屍對象是調試內存管理問題的最佳方式。

打開XCode->Product->Edit Scheme…->Run界面,在Arguments選項卡下添加NSZombieEnabled環境變量並將其值設置爲YES,或者在Diagnostics選項卡下勾選Enable Zombie Objects,即可啓動殭屍對象功能。

殭屍類是從名爲_NSZombie_的模板類裏複製出來的。這些殭屍類沒有多餘的功能,只是充當一個標記,即保留了舊類名。下列僞代碼演示了系統如何根據需要創建殭屍類,而殭屍類又如何把待回收的對象轉化成殭屍對象:

// Obtain the class of the object being deallocated
Class cls = object_getClass(self);

// Get the class's name
const char *clsName = class_getName(cls);

// Prepend _NSZombie_ to the class name
const char *zombieClsName = "_NSZombie_" + clsName;

// See if the specific zombie class exists
Class zombieCls = objc_lookUpClass(zombieClsName);

// If the specific zombie class doesn't exist,
// then it needs to be created
if (!zombieCls) {
// Obtain the template zombie class called _NSZombie_
Class baseZombieCls = objc_lookUpClass("_NSZombie_");

// Duplicate the base zombie class, where the new class's
// name is the prepended string from above
zombieCls = objc_duplicateClass(baseZombieCls,
zombieClsName, 0);
}

// Perform normal destruction of the object being deallocated
objc_destructInstance(self);

// Set the class of the object being deallocated
// to the zombie class
objc_setClass(self, zombieCls);

// The class of 'self' is now _NSZombie_OriginalClass

這個過程其實就是NSObjectdealloc方法所做的事。運行期系統如果發現啓動殭屍對象功能,那麼就把dealloc方法調配(swizzle)成一個會執行上述代碼的版本,將對象所屬的類變爲_NSZombie_OriginClassName,這裏OriginClassName指代原類名。

殭屍類的作用體現在消息轉發例程。_NSZombie_類(以及所有從該類拷貝出來的類)並未實現任何方法。此類沒有超類,跟NSObject一樣都是根類。此類只有一個實例變量isa,所有Objective-C的根類都必須有此變量。由於此類沒有實現任何方法,所有發給它的全部消息都要經過“完整的消息轉發機制”(full forwarding mechanism)。

在完整的消息轉發機制中,___forwarding___是核心。它首先要做的事情就包括檢查接收消息的對象所屬類的名稱,如果前綴爲_NSZombie_,需要特殊處理。此時會打印一條消息,指明殭屍對象所收到的消息和原類名,然後應用程序就終止了。獲取原類名的思路很簡單:將殭屍類名的前綴_NSZombie_去掉即可。打印消息示例如下:

*** -[CFString respondsToSelector:]: message sent to
deallocated instance 0x7ff9e9c080e0

第36條:不要使用retainCount

NSObject協議中定義了下列方法,用於查詢對象當前的引用計數:

- (NSUInteger)retainCount

然而在ARC中不能調用此方法,否則會跟在ARC中調用retain、release和autorelease方法時一樣編輯器會報錯。在MRC(手動引用計數)下仍可使用此方法。

問題在於,保留計數的絕對值一般都與開發者所應留意的事情完全無關。即便在調試時才調用此方法,通常也無所助益。

此方法之所以無用的首要原因在於:它所返回的引用計數值只是某個給定時間點上的值,並未考慮到系統會稍後把自動釋放池清空,因而不會將後續的自動釋放操作從返回值裏減去。如果據此直接將引用計數遞減到0,那麼後續自動釋放池執行清空操作時會導致程序奔潰。

再者,retainCount可能永遠不會返回0,因爲有時系統會優化對象的釋放行爲,在retainCount未歸零的情況下直接將對象回收。

此外,有些常量或變量的retainCount可能不是正常值,例如常量字符串,單例對象,其保留計數永遠不會變。這時的保留和釋放操作都是空操作。

那何時才應該使用retainCount呢?最佳答案是絕對不要用,尤其考慮到蘋果公司在引入ARC之後已正式將其廢棄,就更不應該用了。

發佈了91 篇原創文章 · 獲贊 58 · 訪問量 54萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章