Retain Count

| 11/23/2011
Retain Counting 是 Cocoa 程式設計不可不學的一個記憶體管理機制。一個好的程式員,在對系統記憶體運用以及管理上面做的要是不佳,就很容易出現效能、漏洞、程式崩潰等問題。

所有的 Objective-C 物件都必須要由指標來建立。一般我們很習慣在 C 語言裡面,使用 malloc 產生一塊可以讓指標所指向的「值」。在物件導向設計裡面,我們簡單的稱這個「值」叫做「實體」(instance)。蘋果在設計 Cocoa 的基礎物件 NSObject 裡面,設計了一個簡單的物件方法 (Object Method): +(id)alloc,可以讓產生一個鄉對應物件所需要的一塊實體的記憶體空間,並且回傳實體的指標。當實體被成功建立之後,我們必須要使用 NSObject 實體方法 (Instance Method) 的 -(id)init 來初始化它。這一條基本規則建立起 Retain Counting 所需具備的一切條件。所有繼承 NSObject 的子孫物件都必須要遵守這一條規則。

OWNObject *val;
// call object method    [object_name method_name]
val = [OWNObject alloc];
// call instance method  [var_name method_name]
val = [val init];

Retain Counting 的機制很簡單,物件建立之後,所有繼承 NSObject 的物件都會有一個 retainCount 的 變數 (ivar) 這個變數會在方法 -(id)init 的時候變成 1,-(void)retain 的時候增加, -(void) release 的時候減少。當這個數值變成 0 的時候,物件會自動呼叫 -(void)dealloc,並且將記憶體釋放出來。


// retain count = 1
val = [val init];
// retain count = 2
[val retain]
// retain count = 1
[val release]
// retain count = 0
[val release] -> call [self dealloc] automatically


!記住,千萬不可以自己去呼叫 -(void)dealloc,程式會崩潰!

講到這裡,會有很多人覺得這個跟 Java 的 Reference Counting 的機制不是一樣嗎?其實是不同的。在這裡,記憶體的分配、釋放都是由物件:也就是應用程式本身來執行,而 Java 的 Reference Counting 則是由 Garbage Collocation Daemon 一段時間去掃描所有在記憶體中的物件,找出 Reference Count = 0 的實體並且釋放。這裡雖然遺留了 C 語言在動態記憶體分配之後,程式設計師必須要自己管控記憶體的工作,也有些現代程式語言的精巧。

雖然說蘋果在最新的 LLVM Compiler 3.0 裡面,新增了 Auto Retain Count 的機制,可以讓編譯器幫助你決定在程式碼的哪一個部位 retain、release。但是自動化機制總是會犧牲掉一般程式設計師可以掌握的彈性。我認為理解清楚這個機制,可以有助益於任何記憶體管理方面的進步!


9 則留言:

Unknown 提到...

最近新版的xcode / ios 已經變得好奇怪,
我print出剛init完的retainCount竟然是-1

Ehrippura 提到...

如果你是用 lldb 當 Debugger 的話,試試看改回 GDB 或許會正常一點。

Unknown 提到...

是用simulator, GDB 6.3.50-20050815.

NSString *str = [[NSString alloc] initWithString:@"test~~"];

NSLog(@"count=%d!", [str retainCount]);

===> count=-1!

Ehrippura 提到...

喔,因為你用的是 NSString 來看 retain count。OBJC 的編譯器很聰明,他發現你是用一個 Constant String 去 Initial 他,所以他並不會真的去產生一個新的 NSString 的 Instance,而是系統所產生的 NSConstantString,並且把指標 assign 給你,所以你得到的 retain count 會是 UINT_MAX,也就是 signed int 的 -1。

你可以試試看:
char *s = "test";
NSString *str = [[NSString alloc] initWithCString:s encoding:NSUTF8StringEncoding];

NSLog(@"%d", [str retainCount])
[str release];

Unknown 提到...

原來是這樣,我以為是@autorelease的關係...

剛剛有發現一個怪問題,release完之後,
str的retainCount還可以印,但值還是1 ???
不是應該要歸0? 又或者是應該印了會crash... 因為memory已經free了?

char *s = "test";
NSString *str = [[NSString alloc] initWithCString:s encoding:NSUTF8StringEncoding];

NSLog(@"%d", [str retainCount]);
[str release];

//======
NSLog(@"after release: %d", [str retainCount]);

Ehrippura 提到...

在 debug 階段的時候的 memory management 的行為會跟 release 不一樣。為了讓 programmer 在特殊時候得到某些 object 的信息,autorelease pool 或是 framework 會維持住 object 的 instance。所以 retainCount 這個 method 不太適用在 debug 的設定下。

你可以看到 Xcode 的 Profile build 預設是用 release build,就是這個原因。

Unknown 提到...

謝啦~ 了解~ 獲益良多~
但是初期開發大部分是用simulator...
如果有需要用retainCount查看資源是否正常釋放,不就... 要改用其他方法?

Ehrippura 提到...

我只能說:靠經驗

如果覺得經驗不夠,容易犯錯的話,可以用

product > analyze

不過,程式化的東西都不是百分之百正確的,得要自己判斷。

Unknown 提到...

不好意思,想再請教...

大部分看到範例:
@interface MainViewController : UITableViewController
{
NSMutableArray *menuList;
}

@property (nonatomic, retain) NSMutableArray *menuList;

@end

最近有看到一些範例:
@interface TransitionsViewController : UIViewController

@property (nonatomic, retain) UIView *containerView;
@property (nonatomic, retain) UIImageView *mainView;
@property (nonatomic, retain) UIImageView *flipToView;

- (IBAction)curlAction:(id)sender;
- (IBAction)flipAction:(id)sender;

@end

//======
想請教,這兩種寫法有甚麼差別嗎?