章 11. 記憶體管理

GNUstep 的記憶體管理很重要, 也很簡單. 以下文章值得參考:

  1. Memory Management in Objective-C
  2. Memory Management 101
  3. Very simple rules for memory management in Cocoa
  4. Hold Me, Use Me, Free Me
  5. Accessor methods and (auto)release: conclusion
  6. Memory Management with Cocoa/WebObjects

Objective-C 記憶體管理的基本原則是: 誰創造了物件, 誰就要負責釋放這個物件, 這個 "誰" 便是這個物件的擁有者 (Owndership). 創造物件指的是使用 alloc, 例如:

NSTextField *aTextField = [[NSTextField alloc] init]. 

清除物件的方法就是

[aTextField release];

其中 aTextField 只是一個指標, 指向這個記憶體位置. 因此如果直接使用 [[NSTextField alloc] init], 而沒有指定一個 NSTextField *aTextField, 則 [[NSTextField alloc] init] 所產生出來的物件便永遠無法釋放, 因為無法使用 release 的指令.

有了這個概念之後, Objective-C 記億體管理的方式就算了解了, 相當簡單. 根據這個原則, 任何物件一定要把其產生的物件給釋放. 因此往往在一個類別中, 在 -(id) init 中會產生物件, 在 - (void) dealloc 中會把所有產生的物件給清除. 如果這個物件只在單一類別中使用, 記憶體管理就簡單得多. 再強調一次, NSTextField *aTextField 這類的程式碼只是宣告, 並沒有產生物件, 只有 [NSTextField alloc] 之類的才是真正產生物件的程式碼.

在大部份的情況之中, 物件的位址會被當成 message 傳來傳去, 不論是在同一個物件之中, 或是在不同的物件之間. 當某程式接收到物件時, 因為不是物件的擁有者, 因此不用釋放所接收進來的物件, 同時也無法得知該物件什麼時候會被釋放. 如果該程式是在物件被釋放之後才便用該物件, 那便會出現錯誤, 因為物件已被釋放了, 該記憶體位置已經沒有東西. 同樣的, 當某程式當物件傳遞出去後, 也不知道接收者什麼時候會使用該物件, 如果太早釋放該物件, 則程式也會出現錯誤. 因此在 Objective-C 之中, 如果接收到了一個物件, 並希望該物件能夠一直保留而不會釋放, 則需要使用 retain 將物件留下, 因此習慣上一接收到物件後要馬上將物件 retain 起來. 如果傳遞一個物件出去, 不知道接收者什麼時候會將物件保留起來, 這時不要使用 release 這個指令, 因為 release 會在第一時間釋放物件, 而要改用 autorelease 將物件釋放, 如此接收者才會有時間將該物件保留起來.

在某一個 method 中, 如果有接收物件, 那最好將該物件保留起來, 使用完之後再釋放, 如

 (void) receiver: (id) sender
{
  [sender retain];
  ........
  [sender release];
}

如果在 receiver 中有再將 sender 這個物件傳送出去, 那最好使用 [sender autorelease], 而不是 [sender release], 因為 release 會馬上釋放物件, 而 autorelease 會有較長的緩衝期. 例如

- (NSArray *) receiver
{
  NSArray *aArray = [NSArray new];
  ......
  return [aArray autorelease];
}

[NSArray new] 相當於 [[NSArray alloc] init].

使用 autorelease 時, 要提供一個 autorelease 緩衝區 (Autorelease Pool), 讓 Objective-C 的 Runtime 系統暫時儲存這些表面上已被釋放, 但實際上還沒有被釋放的物件. 使用 NSAutoreleasePool *aPool = [NSAutoreleasePool new]; 即可產生一個緩衝區. 等程式要結束時再用 [aPool release] 釋放緩衝區即可. Objective-C 的 Runtime 系統會自動找到新產生的緩衝區, 所以沒有需要指定物件被放到那個緩衝區的動作.

在一個類別中, 或是一個 method 中, alloc/retain 的次數一定與 release/autorelease 的次數一樣. 如果 alloc/retain 的次數多於 release/autorelease, 則會浪費記憶體 (memory leak), 如果 alloc/retain 的次數少於 release/autorelease, 則程式會出錯 (core dump). 基於這個原則, Objective-C 內部的記憶體管理方式就是計算 alloc/retain/release/autorelease 的次數. 每一個物件內部都有個計數器, 如果該物件被 alloc/retain, 則計數器加一, 如果被 release/autorelease 則減一, 當次數為零時, 表示沒有人在使用這個物件, 則將該物件從記憶體中釋放, 這便是 Objective-C 的記憶體管理方式.

有些物件會被其他物件所共享, 例如

NSArray *oldArray = [NSArray new];
NSArray *newArray = oldArray;

這時 oldArray 與 newArray 指向相同的記憶體位置, 針對 oldArray 所做的改變會影響到 newArray 的內容 (因為是相同的記憶體位置). 如果 newArray 只想取得 oldArray 當時的內容, 而不是與 oldArray 一直保持同步, 則可以使用

NSArray *newArray = [oldArray copy];

copy 如同 alloc/retain 一般, 物件的計數器次數加一.

Objective-C 的記憶體管理原則使得 Objective-C 可以使用類似 Java 的垃圾處理方式. 因此 GNUstep 中定義了一些巨集, 為這個可能性做準備. 在 NSObject.h 中可以找到 RETAIN(), RELEASE(), AUTORELEASE(), DESTORY() 等巨集的定義.

這些程式碼在 GNUstep 中常常看見. 常用的巨集有:

  1. RETAIN(aObject) 等於 [aObject retain].
  2. RELEASE(aObject) 等於 [aObject release].
  3. AUTORELEASE(aObject) 等於 [aObject autorelease].
  4. ASSIGN(aNewObject, aObject) 等於 aNewObject = aObject, 並將 aObject 保留 (retain), 及釋放 (release) 舊的 aNewObject.
  5. ASSIGNCOPY(aNewObject, aObject) 等於 aNewObject = [aObject copy], 並處理好 retian 及 release 的問題.
  6. DESTROY(aObject) 等於 [aObject release], 但是先將 aObject 設為 nil 再處理, 做了層保險.
  7. CREATE_AUTORELEASE_POOL(aPool) 等於 NSAutoreleasePool *aPool = [NSAutoreleasePool new].
  8. RECREATE_AUTORELEASE_POOL(aPool) 會先檢查程式是否已經有緩衝區, 如果沒有時再產生.

使用這些巨集可以提供更謹慎的記憶體管理. 常用的方式如下:

@interface MyObject: NSObject
{
  id myData;
}
-(void) setMyData: (id) newData;
-(id) myData;
@end

@implementation MyObject
- (void) setMyData: (id) newData
{
  ASSIGN(myData, newData);
}

- (id) myData
{
  return myData;
}

- (void) dealloc
{
  RELEASE(myData);
}
@end

ASSIGNCOPY() 可以用來複製物件. 每次要更新 myData 時, 記得使用 [self setMyData: newData] 或是至少使用 ASSIGN(myData, newData). 切勿使用 myData = newData. 如此一來, 便不用耽心記憶體的問題了.

在使用多執行緒時, 可以使用可變動 (mutable)的資料結構, 因為這些 GNUstep 內建的資料結構可以處理多執行的問題:

@interface MyObject: NSObject
{
  NSMutableArray *myArray;
} 
-(void) setMyArray: (NSArray *) newArray;
-(NSArray *) myArray;
@end 

@implementation MyObject
- (id) init
{
  myArray = [NSMutableArray new];
}

- (void) setMyArray: (NSArray *) newArray
{ 
  [myArray setArray: newArray]; 
} 

- (NSArray *) myArray 
{ 
  return myArray; 
} 

- (void) dealloc 
{ 
  RELEASE(myArray); 
}
@end

可變動的資料結構使用較多的系統資源.

要注意 GNUstep 中的傳回值, 有些已經被 autorelease 了. 例如 [NSArray arrayWith...] 或 [@"A string" stringBy...]. 如果再釋於這些物件, 程式會不正常中止. 如果要保留這些物件, 要記得 retain 下來. 可以使用 ASSIGN(), 例如:

ASSIGN(myString, [@"A string", stringBy...])

使用 ASSING() 之後, 記得要在 -dealloc 中釋放