天天看點

IOS中的block和retain cycle

retain cycle 的産生

說到retain cycle,首先要提一下Objective-C的記憶體管理機制。

作為C語言的超集,Objective-C延續了C語言中手動管理記憶體的方式,但是差別于C++的極其非人道的記憶體管理,Objective-C提出了一些機制來減少記憶體管理的難度。 比如:記憶體計數。

在Objective-C中,凡是繼承自NSObject的類都提供了兩種方法,retain和release。當我們調用一個對象的retain時,這個對象的記憶體計數加1,反之,當我們調用release時, 對象的記憶體計數減1,隻有當對象記憶體計數為0時,這個對象才真正會被釋放,此時,對象的delloc方法會被調用來做些記憶體回收前的工作。

記憶體計數機制的好處在于我們可以明确配置設定一個使用權。比如,當一個對象A要使用另外一個對象B的時候,A會retain B一次以表示A使用B,而當B被使用完畢之後,A會 調用B的release方法來放棄使用權。這樣,一個對象可以被多個其他對象使用。而作為使用它的對象,也不必關心自己之外 被使用對象的使用情況(記憶體方面)。一般來講,對于類的成員變量,retain和release分别發生在指派和自身釋放的時候,這就是Obj-C程式中的經典寫法:

頭檔案中:

@property (nonatomic,retain) NSObject *obj;      

 在.m檔案裡:

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

OK,這種方式可以很容易地管理記憶體,但是仍存在這一個問題,這就是retain cycle。

Retain cycle,翻譯成中文大概叫保留環吧。既然父對象持有子對象,而子對象會随父對象釋放而釋放,那麼,如果兩個對象互相為父對象怎麼辦?

比如A和B兩個對象,A持有B,B同時也持有A,按照上面的規則,A隻有B釋放之後才有可能釋放,同樣B隻有A釋放後才可能釋放,當雙方都在等待對方釋放的時候, retain cycle就形成了,結果是,兩個對象都永遠不會被釋放,最終記憶體洩露。

retain cycle使你程式設計的時候不得不注意一些問題。例如,要麼盡量保持子對象引用父對象的時候使用弱引用,也就是assign,比如

@property (nonatomic,assign) NSObject *parent;      

要麼及時地将造成retain cycle中的一個變量設定為nil,将環break掉。如果注意點,這并不是什麼特别大的問題。

嗯,注意點确實不是什麼問題,但是當IOS 4.0隻後,block的出現,使你更需要更為謹慎。

block與記憶體管理

block就是一段可以靈活使用的代碼,你可以把它當變量傳遞,指派,甚至可以把它聲明到函數體裡,更靈活的是你可以在裡面引用外部的環境。 最後一條使得block要有更多的考慮,既然block可以引用外部環境,那如何保證block被調用的時候當時的環境變量不被釋放呢?(block調用的時機可能是随意的)

答案就是,被block引用的變量都會被自動retain一次,這樣的話至少可以保證我們的調用是有效的。

說到這裡你能想到什麼嗎?對,還是retain cycle。因為block中的retain是隐式的,是以極易出現retain cycle的問題。

因為block本身也可以看做一個對象,也存在生命周期,也可以被持有,是以當這種情況出現的時候,我們該注意了,比如:

DoSomethingManager *manager = [[DoSomethingManager alloc] init];
manager.complete = ^{
    //...complete actions
    [manager otherAction];
    [manager release];
};      

retain cycle 就這麼形成了,即使調用了release,manager也不會釋放,因為manager和block互相持有了。為了解除retain cycle的話,我們可以這樣寫:

DoSomethingManager *manager = [[DoSomethingManager alloc] init];
manager.complete = ^{
    //...complete actions
    [manager otherAction];
    manager.complete = nil;
    [manager release];
};      

manager的complete被設定為nil,如此一來retain cycle也被破壞掉,前提是你确實不需要再次回調block了。

本來寫到這裡就算完了,但是新世紀總有新的挑戰,這就在于在Apple有推出了一種新的技術 ARC。

ARC 和 retain cycle

ARC (Auto Reference Counting), 翻譯為自動引用計數,是Apple為了進一步簡化記憶體管理來推出的技術。雖然為自動記憶體管理而生,但卻并算不上真正的自動管理。 這是因為ARC是一種編譯期的技術,它所做的是自動識别你的代碼并轉換成retain/release的形式,在這個層面上來看,ARC無非是簡化了代碼的書寫,并提供了部分性能上的優化, 而并不像Java之類的語言可以完全把垃圾回收抛之腦後(基本上)。關于ARC的細節可以看下面的網址:

http://developer.apple.com/library/ios/#releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html

下面我們主要談下ARC下retain cycle的問題。

ARC中,變量可以用三個關鍵字修飾:

__strong: 指派給這個變量的對象會自動被retain一次,如果在block中引用它,block也會retain它一次。__unsafe_unretained: 指派給這個變量不會被retain,也就是說被他修飾的變量的存在不能保證持有對象的可靠性,它可能已經被釋放了,而且留下了一個不安全的指針。不會被block retain。 __week:類似于__unsafe_unretained,隻是如果所持有的對象被釋放後,變量會自動被設定為nil,這樣更安全些,不過隻在IOS5.0以上的系統支援,同樣不會被block retain。

另外我們也可以用 __block 關鍵字修飾一個變量,表示這個變量能在block中被修改(值修改,而不是修改對象中的某一個屬性,可以了解為修改指針的指向)。會被自動retain。

于其他變量不同的是被 __block 修飾的變量在塊中儲存的是變量的位址。(其他為變量的值)

首先,上面的代碼你現在可以這麼寫:

DoSomethingManager *manager = [[DoSomethingManager alloc] init];
manager.complete = ^{
    //...complete actions
    [manager otherAction];
    manager.complete = nil;
};      

沒什麼問題,隻是去掉了ARC中禁止的release。

當然,我們也可以這麼寫。

__block DoSomethingManager *manager = [[DoSomethingManager alloc] init];
manager.complete = ^{
    //...complete actions
    [manager otherAction];
    manager = nil;
};      

 如果不用ARC,manager不會在block中被retain,但是采用了ARC就有些複雜了。block會retain manager變量,但是,由于__block變量儲存更為底層的變量位址, 是以當此變量被指向其他對象時,block便不對原來的對象負責,引發的結果就是之前對象被release掉,retain cycle被破壞。

或者這麼寫:

__block DoSomethingManager *manager = [[DoSomethingManager alloc] init];
DoSomethingManager __week *weekmanager = manager;
manager.complete = ^{
    //...complete actions
    [weekmanager otherAction];
};      

 上面的__week也可以用 __unsafe_unretained 替代,但是 __week 更安全些,雖然它不支援IOS5.0以下的系統。

被 __week 或者 __unsafe_unretained 修飾的變量不會被block retain,是以不會形成retain cycle,但是小心,保證你的對象不會在complete之前被釋放,否則會得到你意想不到的結果。