Objective-C 記憶體管理

JiandanDream發表於2018-05-09

寫在前面

本文是閱讀 Advanced Memory Management Programming Guide 的筆記。

主要內容是關於手動管理記憶體的規則。

眾所周知,Objective-C 它提供了2種記憶體管理方式:

  1. Manual Retain-release MRR
  2. Automatic Reference Counting ARC

目前 Xcode 預設使用 ARC ,而在 ARC 環境下,很多工作,編譯器已經幫忙完成了。

而要真正瞭解記憶體管理規則,還得追根溯源,從 MRR 出發。

簡介

記憶體管理可能出現的問題

  • 釋放或重寫正在使用的記憶體資料,一般會造成應用閃退,更嚴重地,弄髒使用者資料。
  • 沒有釋放已經不再使用的記憶體,即造成 memory leaks。

記憶體問題檢測工具

Xcode 附帶的靜態分析工具,可以分析出可能有問題的地方。

如果解決了靜態分析工具找到的問題後,仍然有記憶體管理問題,可考慮使用下述工具或技術來定位問題:

  1. 官方除錯技巧,尤其是其中的 NSZombie,可以找回已經釋放了的物件。
  2. 使用 Instruments 去追蹤引用計數情況,以及定位記憶體洩漏。

記憶體管理規則

主要是使用 NSObject 相關的方法 retain, release, dealloc 進行管理。

基本規則

  • 自己建立的物件,自己持有
  • 非自己建立的物件,也能持有
  • 釋放不再需要的某個物件
  • 不能釋放未持有的物件

自己建立的物件,自己持有

當使用以 alloc, new, copy, mutableCopy 開頭的方法,建立物件時,持有該物件。

給某個物件發 retain 訊息後,也能持有它

一般會在2種情況下使用 retain

  • 在 init 方法中,將某個引數作為例項變數
  • 避免某個物件因其他操作而被銷燬。

使用 autorelease 來延遲傳送 release 訊息

- (NSString *)fullName {
    NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@",
                                          self.firstName, self.lastName] autorelease];
    return string;
}
複製程式碼

在上述程式碼中,因為 string 是由 alloc 方法生成的,所以你持有它,當方法結束後,你不再需要它,所以必須在方法結束前將其釋放。

如果使用 release,那麼在方法結束前,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 不是你建立的,所以你沒有持有它,也就不需要釋放它。

覆寫 dealloc 去釋放持有的物件

不能直接呼叫 dealloc 方法。

在 dealloc 方法裡,不要試圖去釋放稀有資源,如網路、快取等。

在 MRC 環境,需要給例項變數傳送 release 訊息,最後需要呼叫 [super release]

Core Foundation 使用類似,但稍微不一樣的規則。

記憶體管理詳細介紹

使用 Accessor Methods 讓記憶體管理更容易

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;
}
複製程式碼

如果你的類有一個屬性是個物件,不妨假設為 P,它由另外一個物件 A 賦值得到,那麼你必須保證 P 在使用過程中,A 不會被銷燬。

所以你必須持有 A,並且在合適時機釋放 A,但這很容易忘記,無疑會增加出問題的概率。

不要在 Initializer Methods 和 dealloc 中使用 Accessor Methods

正確的方式,應該像下面程式碼,直接賦值,不要使用 Setter 方法。

- init {
    self = [super init];
    if (self) {
        _count = [[NSNumber alloc] initWithInteger:0];
    }
    return self;
}
複製程式碼

使用弱引用來避免迴圈引用

如果一個物件收到 retain 訊息,那麼將會有一個強引用指向它。

一個物件只有在沒有任何強引用時,即引用計數為0,才能被銷燬。

當2個物件直接或間接地強引用對方時,它們之間存在一個引用迴圈。

因為都存在強引用,除非在其中物件之一的內部,自動釋放對另一物件的引用,否則兩者都無法被銷燬。

常見的情況就是使用 Block。

避免正在使用的物件被銷燬

有些情況下,物件會被自動銷燬

  1. 當從一個 collection 中移除時。
heisenObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// heisenObject could now be invalid.
複製程式碼

當一個物件從 collection 中,比如 NSArray,被移除時,它會收到 release 訊息,而不是 autorelease 訊息,如果該 collection 是這個物件的唯一持有者,那麼這個物件就會被銷燬。

若想避免這種情況,需要對從 collection 中獲取的物件,傳送 retain 訊息,這樣就能持有它,當不需要時,再釋放。

  1. 當『父物件』被銷燬時
id parent = <#create a parent object#>;
// ...
heisenObject = [parent child] ;
[parent release]; // Or, for example: self.parent = nil;
// heisenObject could now be invalid.
複製程式碼

如上所示,物件 heisenObject 是從物件 parent 中獲得,當 parent 被銷燬時,如果 parent 是 heisenObject 的唯一持有者,那麼 heisenObject 也會被銷燬,相當於在 parent 的 dealloc 方法中,呼叫了 [heisenObject Release]

不要在 dealloc 中管理『稀有』資源

『稀有』資源有檔案描述符、網路連線、緩衝、快取等。

dealloc 何時被呼叫並不明確,有可能會被延時,也有可能是一步步執行的,甚至可能因為一個 bug 而造成應用閃退時,就被呼叫了。

collection 持有它們包含的物件

collection 有 array, dictionary, set 等等,如果一個物件被加入到 collection 時,該物件會呼叫 retain 方法,那麼 collection 持有該物件。

當物件從 collection 中被移除時,該物件會被髮送 release 訊息。

持有規則的實現靠的是引用計數

  • 當你建立一個物件時,它的引用計數為1。
  • 當給一個物件傳送 retain 訊息時,它的引用計數加1。
  • 當給一個物件傳送 release 訊息時,它的引用計數減1。
  • 當給一個物件傳送 autorelease 訊息,它的引用計數會在當前 autorelease pool block 結束時減1。
  • 當一個物件的引用計數為0時,它會被銷燬。

使用 Autorelease Pool Blocks

@autoreleasepool {
    // Code that creates autoreleased objects.
}
複製程式碼

如上述程式碼所示 在 autorelease pool block 即將結束的時候,它當中那些收到過 autorelease 訊息的物件,會被髮送 release 訊息。

即只要一個物件收到過 autorelease 訊息,在當前 autorelease pool block 即將結束時,這個物件就會收到 release 訊息。

autorelease pool block 可以互相巢狀,但比較少用。

Cocoa 希望程式碼都在一個 autorelease pool block 中,否則自動釋放的物件不會被釋放,這樣就會有記憶體洩漏。

如果你在一個 autorelease pool block 外面傳送 autorelease 訊息,那麼 Cocoa 將會報錯。

AppKit 和 UIKit 的每次事件,事實上,都是執行在一個 autorelease pool block 中

事件指的是像一次點選這樣的事件。

所以一般不需要自己建立一個 autorelease pool block,但也有一些情況例外:

  1. 如果你寫的程式,不是基於 UI Framework 的,比如說 command-line tool。
  2. 如果你在每次迴圈裡,建立了大量臨時物件,那麼最好在迴圈裡建立一個 autorelease pool block,來降低應用的記憶體峰值。
  3. 如果你建立了多條執行緒,那麼線上程開始時,你最好建立自己的 autorelease pool block。

在 Cocoa 應用中的每條執行緒,都包含獨立的 autorelease pool block 棧,如果是多執行緒開發,務必建立自己的 autorelease pool block。

相關文章