引用計數與內存管理

轉載自(http://blog.sina.com.cn/s/blog_6de189920100zpxr.html)



內存管理不是一個容易解決的問題,雖然cocoa的解決方案非常簡潔,但是想精通還要費些時日.在Mac OS X v10.5以及之後的版本蘋果新增加了垃圾回收機制用來對內存進行管理.ios開發不支持垃圾回收機制.

在內存管理這塊,cocoa引入了一種稱爲引用計數(reference counting)的技術,有時也叫保留計數,每個對象有一個與之對關聯的整數,稱作它的引用計數器或保留計數器.當某段代碼要訪問一個對象的時候,該代碼將該對象的保留計數值加1,表示"我要訪問該對象",當這段代碼結束對象訪問時,將對象的保留計數值減1,表示它不再訪問該對象,當保留計數值爲0時,表示不再有代碼訪問對象了,對象被銷燬,其佔用的內存被系統回收.


當使用alloc,new方法或者通過copy消息創建一個對象時,對象的保留計數值被設置爲1,要增加對象的保留計數值,可以給對象發送一條retain,減少保留計數值時,發送一條release消息或者autorelease消息.當一個對象保留計數值爲0(即將被銷燬)時,objective-c會自動向發送一條dealloc消息.要獲取保留計數器的數值可以使用retainCount消息來獲取.

- (id)retain;

- (void)release; 

- (id)autorelease;

- (unsigned)retainCount;


對象可以接受任意多的retain和release,只要計數器的值是正的.

MyClass *object = [[MyClass alloc] init];//count =1

[object retain];//count =2

[object retain];//count =3

[object release];//count =2

[object release];//count =1

[object retain];//count =2

[object release];//count =1

[object release];//count =0,dealloc it


這樣看起來可能內存管理很簡單.但是當你考慮對象所有權的時候,就會變的複雜了.

我們先來看下cocoa內存管理規則

(1)當你使用new,alloc,copy方法創建一個對象時,該對象的保留計數值爲1,當不再使用該對象時,你要負責向該對象發送一條release或者autorelease消息.

(2)當你通過任何方法獲得一個對象時,假設該對象保留計數值爲1,而且已經被設置爲自動釋放,你不需要執行任何操作來確保對象被清理.,如果打算要在一段時間被擁有該對象,你需要保留它,並在操作完之後釋放它.

(3)如果你保留了某個對象,你需要釋放或者自動釋放該對象,必須保持retain與release方法的使用次數相等.


autorelease

前面我們強調了,所有使用new, alloc,copy或者是 retain 增加計數器的對象都要用 [auto]release 釋放。記得在 C++ 中,返回值可以在棧上,以便在離開作用域的時候可以自動銷燬。但在 Objective-C 中不存在這種對象。函數使用 alloc 分配的對象,直到將其返回棧之前不能釋放。下面的代碼將解釋這種情況:

// 第一個例子

-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2

{

    Point2D* result = [[Point2D alloc] initWithX:([p1 pointX] + [p2 pointX])

                                            andY:([p1 pointY] + [p2 pointY])];

    return result;

}

// 錯誤!這個函數使用了 alloc,所以它將對象的引用計數器加 1。

// 根據前面的說法,它應該被銷燬,但是這將引起內存泄露:

[calculator add:[calculator add:p1 and:p2] and:p3];

// 調用[calculator add:p1 and:p2],沒有辦法 release。所以引起內存泄露。

 

// 第二個例子

-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2

{

    return [[Point2D alloc] initWithX:([p1 pointX] + [p2 pointX])

                                            andY:([p1 pointY] + [p2 pointY])];

}

// 錯誤!這段代碼實際上和上面的一樣,

// 不同之處在於僅僅減少了一箇中間變量。

 

// 第三個例子

-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2

{

    Point2D* result = [[Point2D alloc] initWithX:([p1 pointX] + [p2 pointX])

                                            andY:([p1 pointY] + [p2 pointY])];

    [result release];

    return result;

}

// 錯誤!顯然,這裏僅僅是在對象創建出來之後立即銷燬了。

這個問題看起來很棘手。如果沒有 autorelease 的確如此。給一個對象發送 autorelease 消息意味着告訴它,在“一段時間之後”銷燬。但是這裏的“一段時間之後”並不意味着“任何時間”。現在,我們有了上面這個問題的一種解決方案:

-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2

{

    Point2D* result = [[Point2D alloc] initWithX:([p1 pointX] + [p2 pointX])

                                            andY:([p1 pointY] + [p2 pointY])];

    [result autorelease];

    return result; // 更簡短的代碼是:return [result autorelease];

}

// 正確!result 將在以後自動釋放


autorelease 池

剛纔我們瞭解到 autorelease 的神奇之處:它能夠在合適的時候自動釋放分配的內存。我們先來了解 autorelease 的機制。當對象收到 autorelease 消息的時候,它會被註冊到一個“autorelease 池”。當這個池被銷燬時,其中的對象也就被實際的銷燬。所以,現在的問題是,這個池如何管理?答案是豐富多彩的:如果你使用 Cocoa 開發 GUI 界面,基本不需要做什麼事情;否則的話,你應該自己創建和銷燬這個池。

擁有圖形界面的應用程序都有一個事件循環。這個循環將等待用戶動作,使應用程序響應動作,然後繼續等待下一個動作。當你使用 Cocoa 創建 GUI 程序時,這個 autorelease 池在事件循環的一次循環開始時被自動創建,然後在循環結束時自動銷燬。如果沒有 GUI,你必須自己建立 autorelease 池。當對象收到 autorelease消息時,它能夠找到最近的 autorelease 池。當池可以被清空時,你可以對這個池使用 release 消息。一般的,命令行界面的 Cocoa 程序都會有如下的代碼:

int main(int argc, char* argv[])

{

    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

    //...

    [pool release];

    return 0;

}

注意在 Mac OS X 10.5 的 NSAutoreleasePool 類新增加了一個 drain 方法。在引用計數的環境中與release等價的.現在有兩種環境:引用計數和垃圾回收。Mac OS 的新版本都會支持垃圾收集器,但是 iOS 卻不支持。在引用計數環境下,NSAutoreleasePool 的 release 方法會給池中的所有對象發送 release 消息,如果對象註冊了多次,就會多次給它發 release。drain 和 release 在應用計數環境下是等價的。在垃圾收集的環境下,release 不做任何事情,drain 則會觸發垃圾收集。

我們在程序可以使用多個 autorelease 池,對象收到 autorelease 消息時會註冊到最近的池。因此,如果一個函數需要創建並使用很大數量臨時對象,爲了提高性能,可以創建一個局部的 autorelease 池。這種情況下,這些臨時變量就可以及時的被銷燬,從而在函數返回時就將內存釋放出來。


autorelease 的注意點

(1)非必要地發送多個 autorelease 類似發送多個 release 消息,在內存池清空時會引起內存錯誤,

(2)即使 release 可以由 autorelease 替代,也不能濫用 autorelease。因爲 autorelease 要比正常的 release 消耗資源更多。另外,不必要的推遲 release 操作無疑會導致佔用大量內存,容易引起內存泄露。


autorelease 和 retain

多虧了 autorelease,方法才能夠創建能夠自動釋放的對象。但是,長時間持有對象是一種很常見的需求。在這種情形下,我們可以向對象發送 retain 消息,使用了 retain 之後,對象的生命期變長了(使用 retain 將使其引用計數器加 1),爲了讓對象能夠正確地被釋放,調用者必須負責後面手動的release將計數器再減 1。



Setters

如果不對 Objective-C 的內存管理機制有深刻的理解,在寫setter的時候也會出現問題。假設一個類有一個名爲 title 的 NSString 類型的屬性,我們希望通過 setter 設置其值。這個例子雖然簡單,但已經表現出 setter 所帶來的主要問題:參數如何使用?


-(void) setString:(NSString*)newString

{

    self->string = newString; // 直接指定

}

外面傳進來的對象僅僅使用引用,不帶有 retain。如果外部對象改變了,當前類也會知 道。也就是說,如果外部對象被釋放掉,而當前類在使用時沒有檢查是否爲 nil,那麼當前類就會持有一個非法引用。


使用 retain 指定

外部對象被引用,並且使用 retain 將其引用計數器加 1。外部對象的改變對於當前類也是可見的,不過,外部對象不能被釋 放,因爲當前類始終持有一個引用。

-(void) setString:(NSString*)newString

{

    self-string = [newString retain]// 使用 retain 指定

}

如下調用:NSString *object = [[NSString alloc] init];//count = 1

[otherObject setString:object];//count = 2

[object release];//count = 1

這時候就出問題了object的保留計數值還是1

使用copy指定,與上面還是一樣的結果.


 

在這種情況下需要釋放舊值纔可以

-(void) setString:(NSString*)newString

{

    [self->string release];

    self->string = [newString retain];

    // 錯誤!如果 newString == string(這是可能的),

    // newString 引用是 1,那麼在 [self->string release] 之後

    // 使用 newString 就是非法的,因爲此時對象已經被釋放

}


但是這時候還是不行,這時候再加一個判斷如下:

-(void) setString:(NSString*)newString

{

    if (self->string != newString)

        [self->string release]// 正確:給 nil 發送 release 是安全的

  self->string = [newString retain];  // 錯誤!應該在 if 裏面

                                        // 因爲如果 string == newString,

                                        // 計數器不會被增加

}

 

// ------ 正確的實現 ------

// 最佳實踐:C++ 程序員一般都會“改變前檢查”

-(void) setString:(NSString*)newString

{

    // 僅在必要時修改

    if (self->string != newString) {

        [self->string release]// 釋放舊的

        self->string = [newString retain]// retain 新的

    }

}

 

// 最佳實踐:自動釋放舊值

-(void) setString:(NSString*)newString

{

    [self->string autorelease]; // 即使 string == newString 也沒有關係,

                                // 因爲 release 是被推遲的

    self->string = [newString retain];

    //... 因此這個 retain 要在 release 之前發生

}

 

// 最佳實踐:先 retain 在 release

-(void) setString:(NSString*)newString

{

    [newString retain]// 引用計數器加 1(除了 nil)

    [self->string release]// release 時不會是 0

    self->string = newString; // 這裏就不應該再加 retain 了

}


Getters

Objective-C 中,所有對象都是動態分配的,使用指針引用。一般的,getter 僅僅返回指針的值,而不應該複製對象。getter 的名字一般和數據成員的名字相同,這並不會引起任何問題。



垃圾收集器

Objective-C 2.0 實現了一個垃圾收集器。換句話說,你可以將所有內存管理交給垃圾收集器,再也不用關心什麼 retain、release 之類。但是,不同於 Java,Objective-C 的垃圾收集器是可選的:你可以選擇關閉它,從而自己管理對象的生命週期;或者你選擇打開,從而減少很多可能有 bug 的代碼。垃圾收集器是以一個程序爲單位的,因此,打開或者關閉都會影響到整個應用程序。

如果開啓垃圾收集器,retain、release 和 autorelease 都被重定義成什麼都不做。因此,在沒有垃圾收集器情況下編寫的代碼可以不做任何改變地移植到有垃圾收集器的環境下,理論上只要重新編譯一遍就可以了。“理論上”意思是,很多情況下涉及到資源釋放處理的時候還是需要特別謹慎地對待。因此,編寫同時滿足兩種情況的代碼是不大容易的,一般開發者都會選擇重新編寫。

由 Apple 開發的 Objective-C 垃圾收集器叫做 AutoZone。這是一個公開的開源庫,我們可以看到起源代碼。不過在 Mac OS X 10.6 中,垃圾收集器可能有了一些變化。

發佈了17 篇原創文章 · 獲贊 5 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章