#osxchat blog

2005/05/13

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,或是 NSDatadataWithContentsOfFile ,在 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;

    作者: Blogger Yen-Ju Chen 發表時間: 5/13/2005 08:07:00 上午  

張貼留言

逆向鍊結:

建立連結

? 回前頁