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之前被釋放,否則會得到你意向不到的結果。