Objective-C記憶體管理:物件

Shanesun發表於2018-09-06

程式的記憶體結構

一個程式記憶體結構可以大致分為2部分:只讀部分和讀寫部分。只讀部分包括.text.rodata段,讀寫部分又根據任務的不同劃分成了以下幾個段:

Objective-C記憶體管理:物件

.text

程式碼段也叫文字段或者文字,儲存了目標檔案的一部分,或者包含虛擬地址空間中的可執行指定。其實就是存放了編譯程式程式碼後機器指令。只讀。

.data

儲存了所有可以修改的全域性變數或者靜態變數,並且這些變數是已經賦了初始值的。

.bss

未初始化全域性變數和靜態變數。

heap

使用 malloc, realloc, 和 free 函式控制的變數,堆在所有的執行緒,共享庫,和動態載入的模組中被共享使用。釋放工作由程式設計師控制,容易產生記憶體洩漏。

連結串列結構,從低地址向高地址生長的不連續記憶體區域,遍歷方向頁是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬記憶體,所以在大量物件建立但沒有及時釋放時會撐爆記憶體,由此可見,堆獲得的空間比較靈活,也比較大。同時因為是連結串列結構,肯定也會造成空間的不連續,從而造成大量的碎片,使程式效率降低。

stack

內部臨時變數以及有關上下文的記憶體區域存放在棧中。程式在呼叫函式時,作業系統會自動通過壓棧和彈棧完成儲存函式現場等操作,不需要程式設計師手動干預。

LIFO結構,從高地址向低地址生長。棧有2種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如區域性變數的分配。動態分配由alloca函式進行分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行釋放,無需我們手工實現。

棧是一塊連續的記憶體區域,棧頂的地址和棧的最大容量是系統預先規定好的。能從棧獲得的空間較小。如果申請的空間超過棧的剩餘空間時,例如遞迴深度過深,將提示stackoverflow。

棧是機器系統提供的資料結構,計算機會在底層對棧提供支援:分配專門的暫存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。

注意:在Objective-C中,函式內的臨時物件,物件指標是放在棧中,而物件資料其實儲存在堆中。 在針對一些場景,比如將這個臨時物件放到一個collection結構中時,在結束作用域時,在堆中更好處理。還有一部分則是歷史原因可以追溯到NeXTSTEP version 2.0時候的設計。 why does objective-c store objects on the heap instead of on the stack

int val = 3;
char string[] = "Hello World";
複製程式碼

這兩個變數的值會一開始被儲存在 .text 中(因為值是寫在程式碼裡面的),在程式啟動時會拷貝到 .data 去區中。

而不初始化的話,像下面這樣,這個變數就會被放在 bss 段中。

static int i;
複製程式碼

資料段

錯誤的記憶體管理會帶來的問題

  1. 使用已經釋放或重寫過的記憶體資料。 會造成資料混亂,通常的結果是crash,設定損壞使用者的資料。
  2. 沒有釋放不再使用的資料,造成的記憶體洩漏。 應用程式在使用過程中不斷增加的記憶體量,這可能導致系統效能不佳或記憶體佔用過多而被終止。

引用計數管理記憶體

iOS中使用了引用計數來管理記憶體,引用計數中包含兩種方式:MRC 和 ARC。這裡假設讀者使用過MRC並且有一定了解。

記憶體管理

記憶體管理原則

在MRC中有四個法則知道程式設計師手動管理記憶體:

  • 自己生成的物件,自己持有。 使用以allocnewcopy或者mutableCopy開頭的方法建立的物件。(比如 alloc,newObject,mutableCopy
  • 非自己生成的物件,自己也能持有。
  • 不在需要自己持有物件的時候,釋放。 通過release或者autorelease訊息釋放自己持有的物件。releaseautorelease在ARC中都不再需要手動呼叫。
  • 非自己持有的物件無需釋放。

四個法則對應的程式碼:

/*
 * 自己生成並持有該物件
 */
 id obj0 = [[NSObeject alloc] init];
 id obj1 = [NSObeject new];
複製程式碼
/*
 * 持有非自己生成的物件
 */
id obj = [NSArray array]; // 非自己生成的物件,且該物件存在,但自己不持有
[obj retain]; // 自己持有物件
複製程式碼
/*
 * 不在需要自己持有的物件的時候,釋放
 */
id obj = [[NSObeject alloc] init]; // 此時持有物件
[obj release]; // 釋放物件
/*
 * 指向物件的指標仍就被保留在obj這個變數中
 * 但物件已經釋放,不可訪問
 */
複製程式碼
/*
 * 非自己持有的物件無法釋放
 */
id obj = [NSArray array]; // 非自己生成的物件,且該物件存在,但自己不持有
[obj release]; // ~~~此時將執行時crash 或編譯器報error~~~ 非 ARC 下,呼叫該方法會導致編譯器報 issues。此操作的行為是未定義的,可能會導致執行時 crash 或者其它未知行為
複製程式碼

非自己持有的物件無法釋放,這些物件什麼時候釋放呢?這就要利用autorelease物件來實現的,autorelease物件不會在作用域結束時立即釋放,而是會加入autoreleasepool釋放池中,應用程式在事件迴圈的每個迴圈開始時在主執行緒上建立一個autoreleasepool,並在迴圈最後呼叫drain將其排出,這時會呼叫autoreleasepool中的每一個物件的release方法。

常量物件

記憶體中的常量物件(類物件,常量字串物件等)是在.data.bss欄位,他們沒有引用計數機制,永遠不能釋放這些物件。給這些物件傳送訊息retainCount後,返回的是NSUIntergerMax。

不要在initdelloc中使用setter或者getter方法

如果存在繼承和子類重寫父類setter或者getter方法的前提下,可能會存在崩潰或異常狀態。

在dealloc裡不要呼叫屬性的存取方法,因為有人可能會覆寫這些方法,並於其中做一些無法再回收階段安全執行的操作(上面已經提到)。此外,屬性可能正處於“鍵值觀察”(Key-Value Observation,KVO)機制的監控之下,該屬性的觀察者(Observer)可能會在屬性值改變時“保留”或使用這個即將回首的物件。這種做法會令執行期系統的狀態完全失調,從而導致一些莫名其妙的錯誤。

不要在delloc中直接管理稀缺資源

物件delloc方法的呼叫可能存在的問題:

  1. 物件release存在兩種可能,一是被強引用依賴其他物件釋放後才會被釋放順序處理,二是如果是autorelease物件,釋放的時機則是無序的。
  2. 物件記憶體洩漏時比較常見但是不會致命的錯誤,但是資源管理對於應該釋放而沒有釋放的資源,引起的問題是可能要更加嚴重。
  3. 如果一個物件在意外時間自動釋放,它將在它碰巧所在的任何執行緒的自動釋放池塊中被釋放。對於只能從一個執行緒觸及的資源,這很容易致命。 所以對於稀缺資源(檔案管理,網路連線,緩衝和緩衝)就不能按照預想的邏輯在delloc中及時處理。

修飾符

屬性修飾符

@property (assign/retain/strong/weak/unsafe_unretained/copy) NSArray *array;
複製程式碼

assign: 表明setter 僅僅是一個簡單的賦值操作,通常用於基本的數值型別,例如CGFloat和NSInteger。 retain: 引用計數加1。 strong: 和retain類似,引用計數加1。物件型別時預設就是strongweak: 和assign類似,當物件釋放時,會自動設定為nilunsafe_unretained: 的語義和assign類似,不過是用於物件型別的,表示一個非擁有(unretained)的,同時也不會在物件被銷燬時置為nil的(unsafe)關係。效能優於weak參照weak實現原理。 copy: 類似storng,不過在賦值時進行copy操作而不是retain,在setter中比較明顯的會發現一個copy行為。 常在model或者賦值時使用,防止外部修改或者多執行緒中修改。

變數修飾符

__strong: 物件預設使用識別符號,retain+1。只要存在strong指標指向一個物件那他就會一直儲存存活。 __weak: 弱引用物件,引用計數不會增加。物件被銷燬時自己被置為 nil 。 __unsafe_unretained: 不會持有物件,引用計數不會增加,但是在物件被銷燬時不會自動置為nil,該指標依舊指向原來的地址,這就變成一個懸垂指標了。 __autoreleasing: 用來表示id *修飾的引數,並且在返回時被自動釋放掉。

// ClassName * qualifier variableName;
// for example:
MyClass * __weak myWeakReference;
MyClass * __unsafe_unretained myUnsafeReference;
複製程式碼

qualifier只能放在 * 和 變數名 之間,但是放到其他位置也不會報錯,編譯器對此做過優化。

在使用引用地址傳值時需要特別注意,比如以下程式碼能正常工作:

NSError *error;
BOOL OK = [myObject performOperationWithError:&error];
if (!OK) {
    // Report the error.
    // ...
}
複製程式碼

但是,這裡有一個錯誤的隱式宣告: NSError * __strong error; 而方法的宣告是: -(BOOL)performOperationWithError:(NSError * __autoreleasing *)error;

因此編譯器會重寫:

NSError * __strong error;
NSError * __autoreleasing tmp = error;
BOOL OK = [myObject performOperationWithError:&tmp];
error = tmp;
if (!OK) {
    // Report the error.
    // ...
}
複製程式碼

當然你也可以建立-(BOOL)performOperationWithError:(NSError * __strong *)error;方法,也可以建立NSError * __autoreleasing error;使他們的型別一致,採用何種方式視具體上下文邏輯而定。

迴圈引用問題

使用引用計數管理記憶體時,不可避免的會遇到迴圈引用問題。 產生原因是多個物件間存在相互引用,其中某個物件的釋放都依賴於另一個物件的釋放,形成了一個獨立的環狀結構。

為了打破這個迴圈引用關係,有以下兩種辦法:

  1. 手動將其中的一條強引用置為nil。
  2. 使用weak弱引用的方式,修飾物件。

AutoreleasePool

上面也有提到了AutoreleasePool,這在整個記憶體管理中扮演了非常重要的角色。 在NSAutoreleasePool文件中:

In a reference counted environment, Cocoa expects there to be an autorelease pool always available. If a pool is not available, autoreleased objects do not get released and you leak memory. In this situation, your program will typically log suitable warning messages.

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event. If you use the Application Kit, you therefore typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop, however, it may be beneficial to create “local” autorelease pools to help to minimize the peak memory footprint.

autoreleasepool 和執行緒的關係 Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see Threads). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself. Threads If you are making Cocoa calls outside of the Application Kit’s main thread—for example if you create a Foundation-only application or if you detach a thread—you need to create your own autorelease pool.

If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should periodically drain and create autorelease pools (like the Application Kit does on the main thread); otherwise, autoreleased objects accumulate and your memory footprint grows. If, however, your detached thread does not make Cocoa calls, you do not need to create an autorelease pool.

文中提到了autoreleasepool4個比較特別的情況:

  1. 如果autoreleasepool無效時,autorelease物件是無法收到release訊息,從而造成記憶體洩漏。在ARC情況下很少會出現autoreleasepool無效的情況下。

  2. 對於需要產生大量臨時autorelease物件的邏輯,需要使用**@autoreleasepool{}**來立即釋放來降低記憶體的峰值。

  3. 關於autoreleasepool線上程中的執行緒的佈局,官方文件說每一個執行緒都會在棧中維護建立的NSAutoreleasePool 物件,並且會把這個物件放到棧的頂部,從而確保線上程結束時autoreleasepool能在最後drain並且dealloc後從棧中移除。

  4. autoreleasepool與執行緒的關係,除了main thread外其他執行緒都沒有自動生成的autoreleasepool。如果你的執行緒需要長時間存活或者會有autorelease物件生成,就必須要線上程一開始就建立autoreleasepool,否則就會有物件洩漏。尤其是長時間存活的執行緒,你還需要像主執行緒在runloop末尾定時的去drain。

記憶體洩漏檢測

  1. 觀察記憶體增長減少情況,在退出介面時,觀察記憶體增長情況。
  2. 檢視物件delloc方法呼叫情況。
  3. Xcode提供的Debug Memory Graph。
  4. Xcode提供的instruments。
  5. MLeaksFinder

參考資料

《Apple About Memory Management》
《Clang中ARC的實現》
《黑幕背後的 Autorelease》
《記憶體管理》

相關文章