寫在前面
本文是閱讀 Advanced Memory Management Programming Guide 的筆記。
主要內(nèi)容是關(guān)于手動管理內(nèi)存的規(guī)則。
眾所周知,Objective-C 它提供了2種內(nèi)存管理方式:
- Manual Retain-release MRR
- Automatic Reference Counting ARC
目前 Xcode 默認(rèn)使用 ARC ,而在 ARC 環(huán)境下,很多工作,編譯器已經(jīng)幫忙完成了。
而要真正了解內(nèi)存管理規(guī)則,還得追根溯源,從 MRR 出發(fā)。
簡介
內(nèi)存管理可能出現(xiàn)的問題
- 釋放或重寫正在使用的內(nèi)存數(shù)據(jù),一般會造成應(yīng)用閃退,更嚴(yán)重地,弄臟用戶數(shù)據(jù)。
- 沒有釋放已經(jīng)不再使用的內(nèi)存,即造成 memory leaks。
內(nèi)存問題檢測工具
Xcode 附帶的靜態(tài)分析工具,可以分析出可能有問題的地方。
如果解決了靜態(tài)分析工具找到的問題后,仍然有內(nèi)存管理問題,可考慮使用下述工具或技術(shù)來定位問題:
- 官方調(diào)試技巧,尤其是其中的 NSZombie,可以找回已經(jīng)釋放了的對象。
- 使用 Instruments 去追蹤引用計數(shù)情況,以及定位內(nèi)存泄漏。
內(nèi)存管理規(guī)則
主要是使用 NSObject 相關(guān)的方法 retain, release, dealloc 進(jìn)行管理。
基本規(guī)則
- 自己創(chuàng)建的對象,自己持有
- 非自己創(chuàng)建的對象,也能持有
- 釋放不再需要的某個對象
- 不能釋放未持有的對象
自己創(chuàng)建的對象,自己持有
當(dāng)使用以 alloc, new, copy, mutableCopy 開頭的方法,創(chuàng)建對象時,持有該對象。
給某個對象發(fā) retain 消息后,也能持有它
一般會在2種情況下使用 retain
- 在 init 方法中,將某個參數(shù)作為實例變量
- 避免某個對象因其他操作而被銷毀。
使用 autorelease 來延遲發(fā)送 release 消息
- (NSString *)fullName {
NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@",
self.firstName, self.lastName] autorelease];
return string;
}
在上述代碼中,因為 string 是由 alloc 方法生成的,所以你持有它,當(dāng)方法結(jié)束后,你不再需要它,所以必須在方法結(jié)束前將其釋放。
如果使用 release,那么在方法結(jié)束前,string 就被銷毀了,根本無法返回。
所以只能使用 autorelease 來延遲釋放它。
沒有持有只是返回引用的對象
NSString *fileName = <#Get a file name#>;
NSError *error;
NSString *string = [[NSString alloc] initWithContentsOfFile:fileName
encoding:NSUTF8StringEncoding error:&error];
if (string == nil) {
// Deal with error...
}
// ...
[string release];
因為 error 不是你創(chuàng)建的,所以你沒有持有它,也就不需要釋放它。
覆寫 dealloc 去釋放持有的對象
不能直接調(diào)用 dealloc 方法。
在 dealloc 方法里,不要試圖去釋放稀有資源,如網(wǎng)絡(luò)、緩存等。
在 MRC 環(huán)境,需要給實例變量發(fā)送 release 消息,最后需要調(diào)用 [super release]。
Core Foundation 使用類似,但稍微不一樣的規(guī)則。
內(nèi)存管理詳細(xì)介紹
使用 Accessor Methods 讓內(nèi)存管理更容易
Accessor Methods 即常說的 Getter 和 Setter 方法
'get' accessor
- (NSNumber *)count {
return _count;
}
'set' accessor
- (void)setCount:(NSNumber *)newCount {
[newCount retain];
[_count release];
// Make the new assignment.
_count = newCount;
}
如果你的類有一個屬性是個對象,不妨假設(shè)為 P,它由另外一個對象 A 賦值得到,那么你必須保證 P 在使用過程中,A 不會被銷毀。
所以你必須持有 A,并且在合適時機釋放 A,但這很容易忘記,無疑會增加出問題的概率。
不要在 Initializer Methods 和 dealloc 中使用 Accessor Methods
正確的方式,應(yīng)該像下面代碼,直接賦值,不要使用 Setter 方法。
- init {
self = [super init];
if (self) {
_count = [[NSNumber alloc] initWithInteger:0];
}
return self;
}
使用弱引用來避免循環(huán)引用
如果一個對象收到 retain 消息,那么將會有一個強引用指向它。
一個對象只有在沒有任何強引用時,即引用計數(shù)為0,才能被銷毀。
當(dāng)2個對象直接或間接地強引用對方時,它們之間存在一個引用循環(huán)。
因為都存在強引用,除非在其中對象之一的內(nèi)部,自動釋放對另一對象的引用,否則兩者都無法被銷毀。
常見的情況就是使用 Block。
避免正在使用的對象被銷毀
有些情況下,對象會被自動銷毀
- 當(dāng)從一個 collection 中移除時。
heisenObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// heisenObject could now be invalid.
當(dāng)一個對象從 collection 中,比如 NSArray,被移除時,它會收到 release 消息,而不是 autorelease 消息,如果該 collection 是這個對象的唯一持有者,那么這個對象就會被銷毀。
若想避免這種情況,需要對從 collection 中獲取的對象,發(fā)送 retain 消息,這樣就能持有它,當(dāng)不需要時,再釋放。
- 當(dāng)『父對象』被銷毀時
id parent = <#create a parent object#>;
// ...
heisenObject = [parent child] ;
[parent release]; // Or, for example: self.parent = nil;
// heisenObject could now be invalid.
如上所示,對象 heisenObject 是從對象 parent 中獲得,當(dāng) parent 被銷毀時,如果 parent 是 heisenObject 的唯一持有者,那么 heisenObject 也會被銷毀,相當(dāng)于在 parent 的 dealloc 方法中,調(diào)用了 [heisenObject Release]
不要在 dealloc 中管理『稀有』資源
『稀有』資源有文件描述符、網(wǎng)絡(luò)連接、緩沖、緩存等。
dealloc 何時被調(diào)用并不明確,有可能會被延時,也有可能是一步步執(zhí)行的,甚至可能因為一個 bug 而造成應(yīng)用閃退時,就被調(diào)用了。
collection 持有它們包含的對象
collection 有 array, dictionary, set 等等,如果一個對象被加入到 collection 時,該對象會調(diào)用 retain 方法,那么 collection 持有該對象。
當(dāng)對象從 collection 中被移除時,該對象會被發(fā)送 release 消息。
持有規(guī)則的實現(xiàn)靠的是引用計數(shù)
- 當(dāng)你創(chuàng)建一個對象時,它的引用計數(shù)為1。
- 當(dāng)給一個對象發(fā)送 retain 消息時,它的引用計數(shù)加1。
- 當(dāng)給一個對象發(fā)送 release 消息時,它的引用計數(shù)減1。
- 當(dāng)給一個對象發(fā)送 autorelease 消息,它的引用計數(shù)會在當(dāng)前 autorelease pool block 結(jié)束時減1。
- 當(dāng)一個對象的引用計數(shù)為0時,它會被銷毀。
使用 Autorelease Pool Blocks
@autoreleasepool {
// Code that creates autoreleased objects.
}
如上述代碼所示
在 autorelease pool block 即將結(jié)束的時候,它當(dāng)中那些收到過 autorelease 消息的對象,會被發(fā)送 release 消息。
即只要一個對象收到過 autorelease 消息,在當(dāng)前 autorelease pool block 即將結(jié)束時,這個對象就會收到 release 消息。
autorelease pool block 可以互相嵌套,但比較少用。
Cocoa 希望代碼都在一個 autorelease pool block 中,否則自動釋放的對象不會被釋放,這樣就會有內(nèi)存泄漏。
如果你在一個 autorelease pool block 外面發(fā)送 autorelease 消息,那么 Cocoa 將會報錯。
AppKit 和 UIKit 的每次事件,事實上,都是運行在一個 autorelease pool block 中
事件指的是像一次點擊這樣的事件。
所以一般不需要自己創(chuàng)建一個 autorelease pool block,但也有一些情況例外:
- 如果你寫的程序,不是基于 UI Framework 的,比如說 command-line tool。
- 如果你在每次循環(huán)里,創(chuàng)建了大量臨時對象,那么最好在循環(huán)里創(chuàng)建一個 autorelease pool block,來降低應(yīng)用的內(nèi)存峰值。
- 如果你創(chuàng)建了多條線程,那么在線程開始時,你最好創(chuàng)建自己的 autorelease pool block。
在 Cocoa 應(yīng)用中的每條線程,都包含獨立的 autorelease pool block 棧,如果是多線程開發(fā),務(wù)必創(chuàng)建自己的 autorelease pool block。