前言
在先前的文章中,我們談到如何建立和使用 Objective-C 物件。限於文章篇幅,當時沒有談到如何管理記憶體。本文延續 Objective-C 物件的主題,說明如何在 Objective-C 程式中管理記憶體。除了沿用原本 C 語言的記憶體管理模式外,Objective-C 發展出數個新的策略,我們會用範例分別展示其寫法。
C 語言的記憶體管理模式
在 C 語言中,記憶體管理有三個模式:
- 靜態記憶體配置 (static memory allocation)
- 自動記憶體配置 (automatic memory allocation)
- 手動記憶體配置 (manual memory allocation)
靜態記憶體用於儲存程式本身、全域變數 (global variable)、靜態變數 (static variable) 等。靜態記憶體不需人為介入,但只能用來初始化固定數值的變數,也無法在執行期動態決定記憶體大小;此外,靜態記體的大小是有限的,故無法把所有的變數都寫到靜態記憶體裡面。濫用全域變數往往造成程式難以維護,故我們會謹慎地使用全域變數。
自動管理的記憶體儲存在堆疊 (stack) 中,會隨著函式的生命週期而消失;除了主函式外,寫在函式中的局部變數 (local variable) 都要注意生命週期的議題,以免得到垃圾值。自動管理的記憶體不需人為介入,但堆疊的容量大小有限,而且自動管理的值有生命週期的問題,所以也不會所有的資料都使用自動記憶體配置。
手動管理的記憶體儲存在堆積 (heap) 中,這些記憶體可以跨越函式傳遞。如同其名,手動管理的記憶體需要人為配置和釋放。由於堆積可使用的記憶體上限約略等於系統所有的記憶體,會是 C 語言中主要的記憶體使用模式。
註:此處的堆疊 (stack) 和堆積 (heap) 不是指資料結構 (data structures),而是記憶體的層級 (layout)。
我們在這篇文章介紹 C 的指標和記憶體管理,有興趣的讀者可以參考一下。
Objective-C 的記憶體管理策略
Objective-C 大部分的物件都是配置在堆積中,故需要人為介入。Objective-C 在發展過程中,發展出以下數種策略:
- 手動記憶體管理
- 垃圾回收 (garbage collection)
- 記憶體池 (memory pool)
- ARC (automatic reference collecting)
圾圾回收是即將棄置的 (deprecated) 特性,故本文不討論。
手動記憶體管理需程式設計師自行配置和釋放記憶體,但 Objective-C 有引入和管理記憶體相關的訊息 (message),而不是用原本在 stdlib.h 中的函式。我們在後文中會有實際的範例來展示如何手動管理記憶體。
記憶體池是半自動的記憶體管理方式。程式設計者需手動建立和釋放記憶體池,物件也要明確地傳入 autorelease
訊息。在 Objective-C 2.0 之前,記憶體池是主要的記憶體管理方式。
ARC 則是 Objective-C 2.0 引入的新特性,可以省下手動管理記憶體的工夫。由於 ARC 僅 Clang 有支援,如果讀者的 Objective-C 程式碼要相容於 GCC 的話,就無法使用這項特性,只能繼續使用記憶體池。我們在後文中也會有範例來展示如何使用 ARC。
手動配置和釋放 Objective-C 物件的記憶體
在 Objective-C 中,同樣也可以手動管理記憶體,但不是用 stdlib.h 中的函式,而是用訊息傳遞的方式。以下是一個假想的例子:
#import <Foundation/Foundation.h> /* 1 */
#include <stdio.h> /* 2 */
int main(void) /* 3 */
{ /* 4 */
/* Allocate a NSMutableDictionary object. */ /* 5 */
NSMutableDictionary *dict = \
[[NSMutableDictionary alloc] init]; /* 6 */
if (!dict) { /* 7 */
perror("Failed to allocate a dictionary\n"); /* 8 */
goto ERROR; /* 9 */
} /* 10 */
/* Do something on `dict`. */ /* 11 */
/* Release the object. */ /* 12 */
[dict release]; /* 13 */
return 0; /* 14 */
ERROR: /* 15 */
/* Release the object only if it exists. */ /* 16 */
if (dict) /* 17 */
[dict release]; /* 18 */
return 1; /* 19 */
} /* 20 */
我們在第 6 行時為 dict
物件配置記憶體及初始化,最後在第 13 行釋放掉 dict
物件所占的記憶體。這和 C 風格的記憶體管理很像,alloc
和 release
的訊息成對出現。
注意 Objective-C 的 alloc
和 release
訊息不能和純 C 的 malloc()
和 free()
函式混用。使用什麼方式配置記憶體,就要用相對應的訊息或函式來釋放記憶體。
但以下的物件則不應手動釋放記憶體:
NSNumber *n = [NSNumber numberWithInteger: 12345];
為什麼會有兩種相異的管理方式呢?這由程式是否對物件有所有權 (ownership) 來決定。
當我們對物件用 alloc
、new
、copy
、mutableCopy
等訊息來產生時,我們對該物件就有所有權,這時候我們就要用 release
訊息歸還所有權。反之,我們使用其他訊息來產生物件時,我們對該物件就沒有所有權,這時候就不需要使用 release
來歸還所有權 (參考 Objective-C 的記憶體管理策略)。
如果我們要確保物件的所有權,可以對物件傳遞 retain
訊息。前述連結有說明使用 retain
的時機,現在暫時用不到,只要知道 Objective-C 的物件有這個訊息即可。
使用記憶體池 (Memory Pool) 半自動管理記憶體
在 Objective-C 2.0 之前,我們要自行在程式中建立記憶池 (memory pool),再搭配 autorelease
訊息來使用。參考下例:
#import <Foundation/Foundation.h> /* 1 */
#include <stdio.h> /* 2 */
#define PRINT(FORMAT, ...) \
fprintf(stderr, "%s\n", \
[[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String]); /* 3 */
int main(void) /* 4 */
{ /* 5 */
/* Create an autorelease pool object. */ /* 6 */
NSAutoreleasePool* pool = \
[[NSAutoreleasePool alloc] init]; /* 7 */
if (!pool) { /* 8 */
perror("Failed to allocate a memory pool\n"); /* 9 */
return 1; /* 10 */
} /* 11 */
/* Create a NSDate object with `autorelease` message. */ /* 12 */
NSDate *now = [[[NSDate alloc] init] autorelease]; /* 13 */
if (!now) { /* 14 */
perror("Failed to allocate a date object\n"); /* 15 */
goto ERROR; /* 16 */
} /* 17 */
PRINT(@"%@", [now description]); /* 18 */
/* Release all objects with `autorelease` message. */ /* 19 */
[pool drain]; /* 20 */
return 0; /* 21 */
ERROR: /* 22 */
[pool drain]; /* 23 */
return 1; /* 24 */
} /* 25 */
我們在第 7 行時建立 pool
物件,在第 20 行時釋放 pool
物件。這算是固定的使用方式。
在建立 autorelease pool 後,要對新建立的物件傳遞 autorelease
訊息,該物件就會在 pool
釋放時一併釋放掉。以本例來說,我們在第 13 行中建立 NSDate
物件時,就一併傳遞了 autorelease
訊息。
使用 ARC (Automatic Reference Counting) 自動管理記憶體 (Clang 限定)
在 Objective-C 2.0 後,引入更方便的 @autorelease
區塊,就不該繼續使用前一節所提到的舊式語法,除非是為了繼續相容 GCC。
我們將先前的例子利用 ARC 改寫如下:
#import <Foundation/Foundation.h>
#define PRINT(FORMAT, ...) \
fprintf(stderr, "%s\n", \
[[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String]);
int main(void)
{
@autoreleasepool
{
NSDate *now = [[NSDate alloc] init];
PRINT(@"%@", [now description]);
}
return 0;
}
由於整個程式碼包在 @autoreleasepool
區塊內,我們就不需要再對 now
物件傳遞 autorelease
訊息,轉由系統自動處理記憶體配置和釋放。
Apple 公司在其官方手冊中提到使用 @autoreleasepool
區塊的效能會比使用記憶體池來得好,如果不需考慮編譯器相容性的話,應該優先使用這個手法管理記憶體。
對 Objective-C 程式檢查記憶體
原本用在 C 語言或 C++ 的知名記憶體檢查軟體 Valgrind 對 GNUstep 程式來說並不太合用,因為 GNUstep 現階段的實作會洩露少量記憶體,且目前沒有要改掉這項臭蟲 (參考這裡)。
替代的方式可以用 Clang 所附帶的 scan-build 對程式碼進行靜態檢查;由於 Clang 支援多個平台,所以這項工具不限於 Mac 平台。像是在 Ubuntu 或 Debian 的 clang-tools 套件中,就包括了該工具,而 MSYS2 則將該工具包在 clang-analyzer 套件中。
在 MSYS2 中開啟 MSYS 終端機環境,輸入安裝指令如下:
$ pacman -S mingw-w64-x86_64-clang-analyzer
使用時,將原本的編譯指令當成 scan-build
的參數即可:
$ scan-build clang -o prog prog.m -lobjc
也可以搭配 Make 等編譯自動化軟體:
$ scan-build make
用 scan-build
檢查程式碼時,若無法完全消除該軟體發出的警告訊息,至少不要出現記憶體洩露的訊息。透過這個程式,應該就可以確保 Objective-C 程式沒有記憶體使用的問題。
結語
在本文中,我們介紹了 Objective-C 的記憶體管理方式。主要的使用考量是使用的編譯器。如果只用 Apple 平台的 Clang 編譯程式,使用 ARC 是最方便的。如果要相容 GCC 的話,則要手動建立記憶體池。