2013年1月12日 星期六

Objective-C memory management

主要參考資料是 Programming in Objective-C 5e ch17, 網路上這篇也滿清楚的。

Objective-C 提供三種管理記憶體的方式:

  • automatic garbage collection: 只有 Mac OS 提供, 且在 OS X 10.8 後宣告 deprecated, 所以不該繼續使用。
  • manual reference counting: 透過一套訂好的 API 和規定函式名稱代表的語意, 讓開發者比較容易管理 reference counting。
  • automatic reference counting: 透過 compiler 生成 reference counting 相關的程式碼, 開發者不用那麼留意 reference counting, 看起來相當美好, 作者極力讚賞這個機制。

Manual Reference Counting (MRC)

NSObject 提供 retain 和 release 兩個方法來 +1 / -1 reference count。所有物件都繼承自 NSObject。開發者要記得如下的規則:

  • 擁有物件的人記得要 +1, 不用時記得要 -1
  • count 為 0 時會呼叫 dealloc, 開發者可自訂 dealloc 釋放自己額外擁有的物件
  • 誰配置這物件, 就是它的初始擁有者
  • 從 copy, mutableCopy, alloc, new 取得的物件, 已經 +1 過了, 不用時記得呼叫 release。有些文章指出有呼叫 init* 開頭的方法表示有 +1, 反之則沒有。像是 [[NSString alloc] initWithUTF8String:X] 之於 [NSString stringWithUTF8String:X] 。這個說法不太正確, 應該是看 alloc 不是看 init。[NSString stringWithUTF8String:X] 是 class method, 不是 instance method, 所以 caller 自己沒有呼叫到 alloc, 自然也沒義務呼叫 release
  • 搭配 @autoreleasepool 可使用 [X autorelease] 先標記「之後記得 -1」。好處是呼叫方法 A 會傳回新物件時, 可在回傳前呼叫 autorelease, 表示方法 A 沒有擁有這個新物件, 且不會讓新物件在傳回前就被消滅。在 @autoreleasepool 的區塊結束時, 會讓所有呼叫過 autorelease 的物件呼叫 release。所以 [NSString stringWithUTF8String:X] 就是呼叫 alloc, 接著呼叫 autorelease, 再傳回新的 NSString
  • 可用 @property(nonatomic, retain) 表示設值的時候, 順便在目標物件上呼叫 retain。第二個參數預設是 assign, 表示沒有只有設 object reference, 沒做額外動作 (如呼叫 retain)

另外兩個和記憶體相關的要點是:

  • 對 nil 呼叫任何方法, 相當於 NOP, 不用擔心程式會掛掉。所以 [nil release] 也是合法的。
  • 從 heap 配置的資料都會初始化為 0, 所以若 instance fields 有指標的時候, 其值皆為 nil。

整體來說, 清楚規則後寫起會滿順手的, 不過需要點時間理清這些規則, 還有因為不是由 compiler 或 runtime 強制處理, 犯錯時不易察覺。

比方說使用 @property(nonatomic, retain) str 時, 在 initStr:s 裡要怎麼設才對?

@interface MyClass : NSObject

@property(nonatomic, retain) NSString *str;

@end


@implementation MyClass

@synthesize str = _str;

-(NSString*) initStrA:(NSString*)s
{
    // 錯誤, 沒有 +1, 之後需要加一行 [_str retain];
    _str = s;
    return self;
}

-(NSString*) initStrB:(NSString*)s
{
    // 錯誤, 多加了一次
    self.str = s;
    [_str retain];
    return self;
}

-(NSString*) initStrC:(NSString*)s
    // 正確, 呼叫到 synthesize 產生的 setter, 
    // 會在設值前呼叫 [s retain] 和 [_str release], 再做 _str = s
    self.str = s;
    return self;
}

備註

寫這篇時手邊沒有 Mac 環境可編譯上述的程式, GNUStep 在 Linux 下編譯的結果會有些錯誤, 但是錯誤來自於對 @property 和 @synthesize 的支援程度不同於 XCode 4.5。也因為這樣碰巧明白 Objective-C 可能不方便跨平台, 在 Mac OS、iOS 以外的平台, 寫起來可能不會很愉快。

Automatic Reference Counting (ARC)

編譯時使用 ARC 的情況下, 除了 compiler 會在 "=" 出現時自動幫右邊 +1, 左邊原始的值 -1 外, compiler 也會知道用 copy, mutableCopy, alloc, new 等方法取得的物件已 +1, 而幫你管對記憶體。除此之外, 還多了 strong/weak pointer 的用法。

想像有個 UI 的 view A, 裡面有個 sub view B (比方說一個視窗裡面有張圖)。A 自然會有 B 的 reference, 而 B 有 A 的 reference 的話, 比較方便執行一些 callback 動作。於是, A 和 B 互指對方 (互相幫對方 +1), 即使沒有任合其它物件用到 A 和 B 時, A、B 仍然不會被回收。

weak pointer 的特色是它不會列入計數, strong pointer (預設) 才會計數。所以, 若 A 用 strong pointer 擁有 B, 而 B 用 weak pointer 擁有 A。那麼, 沒有物件擁有 A 的時候, A 會被回收。此時, weak pointer 的值會自動設為 nil, 所以之後 B 在作操作時可檢查是否為 nil 了解被回收掉了, 或是不檢查直接操作 weak pointer, 也不會讓程式掛掉。當然, 以這例子來說, A 的 dealloc 應該會減少 B 的計數, 若沒有其它物件擁有 B 的話, B 也會被回收掉。

心得

像這樣用 compiler 協助生成程式碼管理 reference counting 頗妙的, 同時滅少 runtime 複雜度和開發者的負擔 (compiler 的心聲: 都沒人在意 compiler 的工作負擔啦...)。讓我對 reference counting 有不同過去死板的認知

此外, 多了 strong 和 weak pointer 的語意, 減少產生 circular reference 的可能性, 並降低物件消滅繼而回收擁有權的難度。舉例來說, 若物件 O 註冊「當 S 有變化時, 記得呼叫 O」。那麼, S 使用 weak pointer 儲存 O 的話, 可以讓 O 在不需擁有 S 的情況下, 自由地於任何時段滅亡, 只要 S 在呼叫 O 時先檢查 pointer 是否變為 nil 即可 (multi-thread 的話, 仍需要 lock 輔助就是了)。

2 則留言:

  1. 大師級的 Mike Ash 這篇也不錯。 http://www.mikeash.com/pyblog/friday-qa-2011-09-30-automatic-reference-counting.html

    回覆刪除
  2. 謝啦, 之後來仔細研究一下, 初步讀了一下, ARC 比原本想像的麻煩一些, 果然實情沒有相當美好, 使用 套件/框架 一定得了解細節才不會踏到雷

    回覆刪除

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...