Objective-C/Cocoa 的記憶體管理小守則
作者: lukhnos
剛開始接觸 Objective-C/Cocoa 程式寫作,最感到困擾的問題之一,大概要屬記憶體管理。Objective-C 最初設計的時候,使用 reference counting (以下簡稱 RC)的記憶體管理模式。事實上是可以讓 Objective-C 有「自動垃圾收集」(automatic garbage collection,以下簡稱 GC)的能力,而且根據 Apple mailing list 的這串討論,Tiger 附的 gcc 4.0 也的確有 GC 的選項。不過,在 Apple 正式宣佈這個重要的突破已經穩定可以全面使用前,大家恐怕還是得先繼續和 Cocoa 的 RC model 相處一陣子。寫過 C 語言的朋友都知道,動態記憶體配置(malloc/free)是一個大課題。該由哪段程式負責配置記憶體,問題大抵還好;該由哪段程式負責把不用的記憶體清掉,就是個大學問。該清的時候不清、忘了清,就會讓記憶體配置堆積(heap)越來越「髒」,不該清的時候清掉了,就會造成記憶體「外洩」(leak),把程式搞當掉。
說來,像 C/C++/Objective-C 這類沒有原生 GC 支援的語言,已經很少了。想得到的新語言,像是 Perl/Python/Ruby,哪個不是內建 GC 的。而且 GC 一點都不新,把 GC 視為理所當然的語言,可以一路上溯至 LISP/Smalltalk。
RC 其實也是 GC 的方法之一,只是在目前的 Objective-C/Cocoa 中,還是得由寫程式的人自己動手做。有人就抱怨:就簡直像是要踩離合器的自動排檔車一樣。當然,也有人覺得 Objective-C/Cocoa 的 RC model 至少還算一致、清楚,雖然 Apple 自己的文件寫得語焉不詳,拉拉雜雜......
於是覺得應該來整理一個「Objective-C/Cocoa 記憶體管理心得」,便有了以下的東西。
規則一:物件的 referecnce count
每一個 NSObject 的衍生類別(也就是 Cocoa 框架裡所有的類別)都有一個名叫 retainCount 的方法,這個
retainCount
傳回的就是物件的 reference count。以下的程式可以取得某個物件的 rc 值:
unsigned rc=[foo retainCount];
規則二:
retain
讓 rc 加一,release
讓 rc 減一,rc 為零時,物件消滅如果將送一個
retain
訊息給物件,物件的 rc 便加一;如果送 release
訊息,則 rc 減一。到 rc 變成 0 的時候,release
便會再送一個 dealloc
訊息給自己,物件的生命期就結束了。規則三:
init*
開頭的 instance method,用完之後 rc 已經等於 1 了凡是在 Objective-C 裡以
init*
開頭的 instance method (就是宣告時前面是「減號」的method),用完之後的物件 rc 都已經是 1 了,因此無需另外再 retain
。規則四:以類別名稱開頭的 method (例如
NSString
的 +stringWithUTF8String
, -stringByAppendingString
),傳回的物件要 retain
在 Cocoa 裡,所有以類別名稱開頭的 method ,例如上述兩個
NSString
methods,或是 NSData
的 dataWithContentsOfFile
,在 Cocoa 裡通稱 "convenience method" ,它們都會傳回一個新建立的物件。如果你要留下這些物件,請務必呼叫 retain
(原因請參見下面的規則七)。規則五:別人傳給你的物件,要留下來的話,請複製一份,或是
retain
如果你自己寫了新類別,在寫 init* method 時要收別人傳給你的物件,那麼請自己
retain
一份。如果傳的是像字串這種可能被更改內容的物件,則請自己 copy 一份。例如:
@interface foo : NSObject {
NSString *m_name;
}
-(id) initWithName:(NSString*)name;
@end;
@implementation foo
-(id) initWithName:(NSString*)name {
self=[super init];
if (self) {
m_name=name;
[m_name retain];
}
return self;
}
-(void) dealloc {
[m_name release];
[super dealloc];
}
@end;
在 Objective-C 裡,繼承下來的方法,是不需要再度於
@interface
段中宣告的。規則六:丟進 autorelease pool 裡的物件,不久之後就會被
release
掉了autorelease pool 是 Cocoa 裡最奇怪的設計之一。但是瞭解設計緣由之後,就會覺得還蠻有道理的。有時候我們並不想理會這物件何時被
release
。這物件可能只是暫時被生來做個簡單的事情。autorelease pool 其實就是個大陣列,當 pool 生命期結束的時候,便會送 release
訊息給所有 pool 裡的物件。如果 pool 裡的物件還被其他段落的程式給 retain
著,就不會被清掉。在 Cocoa 的程式裡,每一輪 event loop 都會建立一次 autorelease pool ,並把先前建立的 pool 給清掉。
簡言之,autorelease pool 就是某種「定期清掃」的「池子」。如果你不想管這個物件的生命期,只要丟個 autorelease 訊息,物件就會被放進 pool 裡了:
[foo autorelease];
規則七:所有以類別名稱開頭的 method 建立的物件,全都在建立完後,被放進 autorelease pool 裡。
這就是為什麼先前規則四裡有說,如果你要留下物件,要自己 retain。因為如果不這樣做,物件在程式執行若干段落後(例如回到
NSApplication
的 event loop 後),就會消失不見了。許多
NSString
, NSData
, NSURL
的物件,都具有「用過就丟」的特質。因為 Cocoa 的 autorelease pool,因此我們不用在乎物件何時被釋放。這樣做的好處是相當相當明顯的,只要想想以下程式碼就好了:
int bar(const char *p) {
NSString *q=[[NSString alloc] initWithUTF8String:p];
// ...
if (SOME_ERR) {
[q release];
return 0;
}
// ...
if (ANOTHER_ERR) {
[q release];
return 0;
}
// ...
[q release];
return 1;
}
上述程式中,我們可能要處理各種不同的錯誤,每遇到一次錯誤,
bar()
就得提前結束,並把 bar()
之中所建立的物件清掉。如果沒有 autorelease pool,就會相當麻煩。但因為有 autorelease pool 的關係,而且既然 q
只在 bar()
之中作用,那麼我們可以把程式改寫成下列的樣子,不用再去理會 q
何時會被消滅掉:
int bar(const char *p) {
NSString *q=[NSString stringWithUTF8String:p];
// ...
if (SOME_ERR) {
return 0;
}
// ...
if (ANOTHER_ERR) {
return 0;
}
// ...
return 1;
}
1 篇留言:
物件也可以在 method 結束前就 autorelease。
int bar(const char *p) {
NSString *q=[[NSString alloc] initWithUTF8String:p];
[q autorelease];
// ...
if (SOME_ERR) {
return 0;
}
// ...
if (ANOTHER_ERR) {
return 0;
}
// ...
return 1;
}
使用巨集可以省掉一些麻煩。
@interface foo : NSObject {
NSString *m_name;
}
-(id) initWithName:(NSString*)name;
@end;
@implementation foo
-(id) initWithName:(NSString*)name {
self=[super init];
if (self) {
ASSIGN(m_name, name);
}
return self;
}
-(void) dealloc {
RELEASE(m_name);
[super dealloc];
}
@end;
作者: Yen-Ju Chen 發表時間: 5/13/2005 08:07:00 上午
張貼留言
? 回前頁