閱讀770 返回首頁    go 技術社區[雲棲]


obj-c編程11:內存管理和ARC(自動引用計數)

    乖乖隆地洞,這篇文章內容可是不得了,內存管理哦!首先,這個要是搞不明白,你就等著進程莫名其妙的掛死,或是瘋狂申請內存卻不釋放,結果被OS殺死,不管是“自殺”還是“他殺”,都不是那麼好玩的哦。其次要記住這可不是windows 中的內存管理(Win32 api),也不是linux中C like的內存管理方法。這個比他們都“高級”的多啊!但是沒有ruby的高級,也沒有ruby的簡單,如果mac編程用ruby的就好了,這不搞出一個雨燕(SWFIT)來啊!

    在Xcode4.2發布前,內存管理的確是令人恐怖的主題,都把細節推給我們碼農了啊。隨著4.2的發布,加入了新的自動應用計數特性(Automatic Reference Counting,ARC),碼農門不再脫發著思考內存管理問題鳥。

    obj-c基本內存管理模型有以下3種:

1 自動垃圾收集

2 收工引用計數和自動釋放池

3 自動引用計數(ARC)

我們依次來看一下吧。


【1】自動垃圾收集

存在於obj-c 2.0中,iOS裏不支持垃圾收集(不知現在是否如此哦),僅os x支持。當進程運行到某個低內存的臨界點時,自動開始清理垃圾,這是一個計算密集過程,可能導致進程掛起,所以不推薦使用該特性鳥。


【2】手動管理內存計數

一般的當對象創建時,初始引用計數為1,以後每當引用一次該對象需要為該對象的引用計數加1,可以給該對象發送retain消息:

[myobj retain];

當不在需要該對象時,給其發送release消息使其引用計數減1:

[myobj release];

當對象引用計數為0時,理論上該對象不會再被使用,因為沒有東西引用到它,他徹底淪為“宅對象”嘍。所以可以釋放其內存,通過給該對象發送dealloc消息完成這個操作。大多數情況下對象會繼承NSObject的dealloc方法,當然如果類覆寫了dealloc方法完成自己的析構,則當仁不讓的會調用新的方法。

    在手工管理引用計數時,要注意F庫中一些方法會隱式的增加對象的引用計數,同樣一些方法會隱式減少引用計數。

  引用計數為0的引用稱為懸掛指針(dangling pointer)的引用。如果給已釋放的對象發送release消息,則會引起過度釋放對象,會導致程序崩潰。

2.1自動釋放池:設想下這種情況:方法返回一個在其中alloc的對象。這時雖然該方法不再使用這個對象,但不能釋放它,因為該對象會作為該方法的返回值。NSAutoreleasePool類創建的目的就是希望能解決這個問題。通過給自動釋放池發送drain消息,自動釋放池中的對象會被清理和釋放。

    通過給對象發送autorelease消息,可以將其手動添加到自動釋放池維護的對象列表中:[my_obj autorelease];

並不是所有新創建的對象都會被自動添加到自動釋放池中,任何以alloc,copy,mutableCopy和new為前綴的方法創建的對象都不會被自動添加到池中。在這種情況下,我們說你擁有這個對象。當你擁有一個對象時,你必須自己負責這個對象,so你需要在使用完後自己釋放這些對象的內存:主動給對象發送release消息或發送autorelease將其加入到自動釋放池中,下麵上代碼:

#import <Foundation/Foundation.h>

int main(int argc,char *argv[])
{
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	NSString *str0 = [[NSString alloc] init];

	//alloc方法不會自動加到池中,所以要手動釋放
	[str0 release];

	[pool drain];
	return 0;
}

此代碼表明在代碼開始位置創建自動釋放池,在進程返回前,代碼結束位置清理自動釋放池。值得注意的是以上代碼編譯時不可以加 -fobjc-arc選項,也就是不能與ARC機製同時使用哦,否則編譯會出錯的:

apple@kissAir: objc_src$clang -fobjc-arc -framework Foundation 5.m -o 5

5.m:5:2: error: 'NSAutoreleasePool' is unavailable: not available in automatic

      reference counting mode

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

        ^

/System/Library/Frameworks/Foundation.framework/Headers/NSAutoreleasePool.h:8:12:note: 

      declaration has been explicitly marked unavailable here

@interface NSAutoreleasePool : NSObject {

           ^


在遇到將一個對象作為方法的返回時,我們可以用明確的代碼來表示將其放入自動釋放池中,確保當起引用計數為0時且pool收到drain消息時能夠自動釋放:

//in some method:

Some_class *ret = [[Some_class alloc] init] autorelease];

或者在實際返回時再加入:

return [ret autorelease];

2.2 事件循環和內存分配:Cocoa和ios進程運行在事件循環中,類似於windows的事件循環,比如按下一個按鈕事件。每當一個新事件發生時,係統會創建一個新的自動釋放池,然後可能會調用你代碼中的一些方法來處理該事件。當處理完事件,並從你的方法返回後,係統在等待下一個事件發生的間隙,會清理自動釋放池哦。這意味著除非對象使用retain否則無法在清空自動釋放池的過程中存活下來啦。下麵看一個例子:

#import <UIKit/UIKit.h>
@interface myView:UIView
	@property (nonatomic,retain)NSMutableArray *data;
@end

假設在實現內部已經用synthesize對data屬性進行了同步,再假定viewDidLoad方法會在視圖載入內存是被係統調用,則在該方法中有:

data = [NSMutableArray array];

可是這樣有個問題,就是前麵提到的F庫得方法會默認創建自動釋放的對象,array創建的是一個自動釋放的數組,該數組會在當前事件結束後被立即釋放。為了保證數組在事件循環中還能存在,可以使用下麵3中方法中的任何一種:

1. data = [[NSMutableArray array] retain]; //用retain後引用不會為0,所以不會被釋放掉哦

2. data = [[NSMutableArray alloc] init]; //前麵說過的alloc開頭的方法不會加入自動釋放池中哦

3. self.data = [NSMutableArray array];

注意最後一種方法,沒有直接使用實例變量data,而是通過data屬性賦值的,而data屬性前麵使用了retain特性,所以自動釋放的數組會被保持。但是不管用3種方法的哪一種,都需要覆蓋dealloc方法用於在實際銷毀myView對象時釋放數組哦:

-(void)dealloc{
	[data release];
	[super dealloc];
}

還是要記得調用父類中的dealloc啊!在手工引用計數環境中,可以為屬性添加atomic(默認)或nonatomic特性,也可以添加assign(默認),retain和copy特性。當使用設置方法為屬性賦值時我們來看看assign,retain和copy 3種特性實現上的不同:

self.property = new_value;
//assign way:
property = new_value;

//retain way:
if(property != new_value){
	[property release];
	property = [new_value retain];
}

//copy way:
if(property != new_value){
	[property release];
	property = [new_value copy];
}
最後對手工內存管理(即不使用垃圾回收或ARC特性)規則做一個總結:

1 如果需要保持一個對象不被銷毀,可以使用retain,但在使用完對象後需要手動release進行釋放;

2 給對象發送release不一定會銷毀該對象,除非該對象的引用計數等於0,這時係統會發生dealloc消息給該對象;

3 對於使用了retain,copy,mutableCopy,alloc或new方法的任何對象,以及具有retain和copy特性的屬性進行釋放時,需要覆寫dealloc方法;

4 如果在方法中返回一個對象(該方法不需要該對象),則可以給該對象發送autorelease消息標記這個對象延遲釋放,autorelease消息不會影響到對象的引用計數;

5 當進程終止時,該進程內存中的所有對象都會被釋放(貌似是廢話);

6 當開發cocoa或iOS程序時,自動釋放池會隨著每次事件發送而創建和清空,在此情況下,如果要使自動釋放池被清空後自動釋放的對象還能夠存在,對象需要使用retain方法。隻要對象的引用計數大於發送autorelease消息的數量,就能夠在池清理後生存下來。


【3】ARC

    自動引用計數ARC可以避免收工引用計數的一些潛在陷阱,但原來的引用計數仍然被維護和跟蹤。然而係統會檢測出何時需要保持對象,合適需要釋放對象,這些你都不用擔心鳥。你也不必擔心返回了方法內創建的對象,編譯器會管理好對戲的內存,編譯器會通過生成正確的代碼去自動釋放或保持返回的對象(對於其他對象也是類似)。我們首先要引出強變量和弱變量2個概念:

強變量:通常在ARC中所有指針變量都是強變量。將對象引用obj_new賦值給obj_old會使obj_new對象自動保持,同時舊對象obj_old會在被賦值千被釋放。強變量默認會被初始化為0,無論他是實例變量、局部變量還是全局變量這都成立。我們看以下代碼:

Some_class *obj0 = [[Some_class alloc] init];
Some_class *obj1 = [[Some_class alloc] init];

obj0 = obj1;

當手工管理內存時上述代碼會導致obj0對象的引用丟失,隨後他的值被覆蓋,從而產生內存泄露,即一個變量不再被引用,但又不能夠釋放。如果使用ARC,obj0和obj1都是強變量,前麵賦值其實會是這樣:

[obj1 retain];
[obj0 release];
obj0 = obj1;
但你不會在代碼中實際看到上述代碼,編譯器在後台幫你做了這些,你隻要寫賦值語句就行啦。

因為所有對象默認都是強變量,所以不需要先聲明,但你仍然可以使用關鍵字__strong:

__strong Some_class *obj;

但是默認屬性不是strong,而是unsafe_unretained(相當於assign),所以如果有必要你需要這樣為屬性聲明strong特性:

@property(strong,nonatomic) Some_class *obj;

編譯器會保證事件循環中通過強屬性對賦值執行保持操作,從而屬性對象可以存活下來。帶有unsafe_unretained(相當於assign)或weak的屬性不會執行這些操作。

弱變量:在一些情況下,2個對象都持有彼此的強引用時,會產生循環保持(retain cycle),這樣2個對象都不可以被銷毀,即使其中一個已經不再被使用了。解決這個問題可以通過在2個對象間建立弱引用的方法解決。我們可以在父對象到子對象間使用強引用,而子對象到父對象間使用弱引用,這樣就沒有循環保持,弱變量也不能阻止引用的對象被銷毀,在這裏即是子對象無法阻止父對象被銷毀。此時子對象會被係統自動設置為nil,這也避免了無意給它發送消息引起崩潰的問題。因為給nil對象發送消息不會有反應哦。

    可以使用__weak關鍵字聲明一個弱變量:

__weak Some_class *obj;

或者為屬性指定weak特性:

@property(weak,nonatomic) Some_class *obj;

需要注意的是,在ios4和os x 10.6中不支持弱變量,此時你仍然可以為屬性使用unsafe_unretained(或assign)特性,或者將變量聲明為__unsafe_unretained,然而這時當引用的對象被銷毀時,變量不再被清零嘍(即被置為nil)。

細心地看客或許已經注意到,本係列到目前為止幾乎所有代碼都會在main中有@autoreleasepool指令,該指令圍住的語句塊定義了自動釋放池的上下文,在自動釋放池塊結束的時候,任何在這個上下文中創建的對象都會被自動銷毀。(除非編譯器在自動釋放塊結束後還需要保證這個對象存在)我們可以在產生大量臨時對象的地方使用這一機製:

for(i = 0;i < n;++i){
  @autoreleasepool{
    //處理大量臨時變量
  }
}

在本文開始處提到,cocoa和ios應用運行在事件循環中,為了處理新的事件,係統會創建一個新的自動釋放池上下文,在事件結束的時候,自動釋放池上下文已經結束,意味著自動釋放對象可能被銷毀。使用ARC,這些都會在“底層”發生,你無需為此擔心。

    ARC很好,但其與ARC之前的代碼兼容性如何呢?我們說隻要非ARC代碼與標準的cocoa命名規則一致,都會運行良好。當ARC遇到方法調用時會檢查方法名,如果名字以alloc、new、copy、mutableCopy或init開頭時,它會假定這些方法返回對象的所有者給方法的調用者。(即意思為這些返回的對象需要自己手動釋放?ARC不會自動釋放這些對象?)除非你使用的方法不符合標準的命名規則,此時,需要使用隱性通知編譯器該方法會返回對象的擁有者(以便讓編譯器不會自動釋放返回的方法?)。最後要注意的的是,如果你試圖合成屬性,而屬性的名字是以上麵提到的特殊詞開頭的話,編譯器會提示一些錯誤哦。

    


最後更新:2017-04-03 05:38:55

  上一篇:go 技術敏感度 — 基層技術管理者必備
  下一篇:go 從產品角度考慮優秀員工的評定