1.什麼是自動引用計數? 顧明思義,自動引用計數(ARC,Automatic Reference Counting)是指記憶體管理中對引用採取自動計數的技術。 在OC中採用ARC機制,讓編譯器來進行記憶體管理。在新一代apple LLVM編譯器中設置ARC為有效狀態,就無需再次鍵入retain或者rel ...
1.什麼是自動引用計數?
顧明思義,自動引用計數(ARC,Automatic Reference Counting)是指記憶體管理中對引用採取自動計數的技術。
在OC中採用ARC機制,讓編譯器來進行記憶體管理。在新一代apple LLVM編譯器中設置ARC為有效狀態,就無需再次鍵入retain或者release代碼,這在降低程式崩潰、記憶體泄露等風險的同時,很大程度上減少了開發程式的工作量。編譯器完全清楚目標對象,並能立刻釋放那些不在被使用的對象。
1.2 記憶體管理/引用計數
1.2.1 概要
OC中的記憶體管理,也就是引用計數。可以用開關房間的等為例來說明引用計數的機制,比如:上班進入辦公室需要照明,下班離開辦公室不需要照明。
假設辦公室里的照明設備只有一個。上班進入辦公室的人需要照明,所以要把燈打開,而對於下班離開辦公室的人來說,已經不需要照明瞭,所以要把燈關掉。若是很多人上下班,每個人都開燈或是關燈,那麼辦公室的情況又將如何呢?最早下班離開的人如果關了燈,辦公室里還沒有走的人都將處於一片黑暗當中。
解決這個問題的辦法是使辦公室在還有至少1人的情況下保持開燈狀態,而在無人時保持關燈狀態。
a.第一個人進入辦公室,“需要照明的人數”加1,計數值從0變成了1,因此要開燈。
b.之後每當有人進入辦公室,"需要照明的人數"就加1,計數值從1變成2.
c.每當有人下班離開辦公室,"需要照明的人數"就減1。如計數值從2變成1.
d.最後一個人下班離開辦公室時,"需要照明的人數"減1,計數值從1變成了0,因此要關燈。
對辦公室照明設備所做的動作和對OC對象所做的動作的對比。
對照明設備所做的動作:開燈 - 需要照明 - 不需要照明 - 關燈
對OC對象所做的動作: 生成對象 - 持有對象 - 釋放對象 - 廢棄對象。
使用計數功能計算需要照明的人數,使辦公室的照明得到了很好的管理。同樣,使用引用計數功能,對象也就能夠得到很好的管理,這就是OC的記憶體管理。
1.2.2 記憶體管理的思考方式
自己生成的對象,自己所持有。
非自己生成的對象,自己也能持有。
不在需要自己持有的對象時釋放。
非自己持有的對象無法釋放。
// 對應的方法
對象操作:生成並持有對象 - 持有對象 - 釋放對象 - 廢棄對象
OC方法 :alloc/new/copy/mutablecopy - retain - release - dealloc
這些有關OC記憶體管理的方法,實際上不包括在該語言中,而是包含在cocoa框架中,用於ios開發。cocoa框架中Foundation框架類庫的NSObject類擔負記憶體管理的職責。OC記憶體管理中的alloc/retain/release/dealloc方法分別指代NSObject類的alloc類方法、retain實例方法、release實例方法和dealloc實例方法。
自己生成對象,自己所持有
使用alloc new copy mutableCopy 開頭的方法名意味著自己生成的對象只有自己持有。本文所說的“自己”固然對應前文提到的“對象的使用環境”,但將之理解為編程人員“自身”也是沒錯的。下麵寫出了自己生成並持有對象的代碼,為生成並持有對象,我們使用alloc 方法。
// 自己生成並持有對象 id obj = [[NSObject alloc] init];
// 使用NSObject類的alloc類方法就能自己生成並持有對象。指向生成並持有對象的指針被賦給變數obj,另外,使用如下new類方法也能生成並持有對象。
// 自己生成並持有對象
id obj = [NSObject new];
copy 方法利用基於NSCopying方法約定,由各類實現的copyWithZone,方法生成並持有對象的副本。與copy方法類似,mutableCopy方法利用基於NSMutableCopying方法約定,由各類實現的mutableCopyWithZone方法生成並持有對象的副本。兩者的區別在於,copy方法生成不可變更的對象,而mutableCopy方法生成可變更的對象。這類似於NSArray和NSMutableArray的差異。
allocMyObject newThatObject copyThis mutableCopyYourObject 方法也意味著自己生成並持有對象。
但是allocate newer copying mutableCopyed 並不屬於同一類別的方法。
非自己生成的對象,自己也能持有
用上述項目之外的方法取得的對象,即用alloc/new/copy/mutableCopy 以外的方法取得的對象,因為非自己生成並持有,所以自己不是該對象的持有者。我們來使用NSMutableArray類的array類方法。
// 取得非自己生成並持有的對象 id obj = [NSMutableArray array]; // 取得的對象存在,但是自己不持有對象,源代碼中,NSMutableArray類對象被賦值給obj,但變數obj自己並不持有該對象,使用retain方法可以持有對象。 // 取得非自己生成並持有的對象,取得的對象雖在,但是自己並不持有 id obj = [NSMutableArray array]; // 自己持有對象,通過retain方法,非自己生成的對象跟用alloc/new/copy/mutableCopy方法生成並持有的對象一樣,成為了自己所持有的。 [obj retain]
不需要自己持有的對象時釋放
自己持有的對象,一旦不再需要,持有者有義務釋放該對象,釋放使用release方法。
// 自己生成並持有對象 id obj = [[NSObject alloc] init]; // 釋放對象,指向對象的指針仍然被保留在變數obj中,貌似能夠訪問。但是對象一經釋放絕對不可以訪問。 如此,用alloc方法由自己生成並持有的對象就通過release方法釋放了,自己生成而非自己所持有的對象,若用retain方法變為自己持有,也同樣可以用release方法釋放。 // 取得非自己生成並持有的對象 id obj = [NSMutableArray array] // 自己持有對象 [obj retain] // 自己釋放對象,對象不可再被訪問 [obj release]
如果要用某個方法生成對象,並將其返還給該方法的調用方,那麼他的源代碼又是什麼樣的呢?
// 如下代碼,原封不動地返回用alloc方法生成並持有的對象,就能讓調用方也持有該對象。allocObject 名稱符合命名規則,因此它與用alloc方法生成並持有對象的情況完全相同,所以使用allocObject方法也就意味著“自己生成並持有對象” - (id)allocObject{ // 自己生成並持有對象 id obj = [[NSObject alloc] init]; // 自己持有對象 return obj; } // 調用[NSMutableArray array] 方法使取得的對象存在,但自己不持有對象,又是如何實現的?根據上文的命名規則,不能使用以alloc/new/copy/mutableCopy開頭的方法名,因此要使用object這個方法名。 - (id)object{ id obj = [[NSObject alloc] init]; [obj autorelease]; return obj; } // 上例中,我們使用了autorelease方法,用該方法,可以使取得的對象存在,但自己不持有對象。
autorelease 提供這樣的功能,使對象在超出指定的生存範圍時能夠自動並正確的釋放(調用release方法)下麵是release和autorelease的區別
無法釋放非自己持有的對象
對於非alloc/new/copy/mutableCopy方法生成並持有的對象,或是用retain方法持有的對象,由於持有者是自己,所以在不需要該對象時需要將其釋放。而由此之外所得到的對象絕對不能釋放。倘若在應用程式中釋放了非自己所持有的對象就會造成崩潰。例如自己生成並持有對象後,在釋放完不再徐亞ode對象之後再次釋放。
// 自己生成並持有對象 id obj = [[NSObject alloc] init]; // 對象已釋放 [obj release]; // 釋放之後再次釋放已非自己持有的對象,應用程式崩潰,崩潰情況,再度廢棄已經廢棄了的對象時崩潰,訪問已經廢棄的對象時崩潰。 ---------------------------------------------------------------------------------------------------------- // 取得的對象存在,但自己不持有對象 id objq = [obj0 object] // 釋放了非自己持有的對象,這肯定會導致應用程式崩潰
如這些例子所示,釋放非自己持有的對象會造成程式崩潰。因此絕對不要去釋放非自己持有的對象。
1.2.3 alloc/retain/release/dealloc 實現
包含NSObject類的Foundation框架並沒有公開,不過,Foundation框架使用的Core Foundation框架的源代碼,以及通過調用NSObject類進行記憶體管理部分的源代碼是公開的。但是,沒有NSObject類的源代碼,就很難瞭解NSObject類的內部實現細節,所以,我們首先使用開源軟體GNUstep來說明。
GNUstep是Cocoa框架的互換框架,也就是說,GUNstep的源代碼雖不能說和Cocoa實現完全相同,但是從使用者角度來看,兩者的行為和實現方式是一樣的,或者說非常相似,
id obj = [NSObject alloc]; // 這裡調用了NSObject類的alloc類方法在NSObject.m源代碼中的實現如下。 GNUstep/modules/core/base/Source/NSObject.m alloc +(id)alloc{ return [self allocWithZone: NSDefaultMallocZone( )]; } +(id)allocWithZone: (NSZone *)z{ return NSAllocateObject (self, 0 , z); } // 通過allocWithZone類方法調用NSAllocateObject函數分配了對象,下麵我們看看NSAllocateObject函數。 GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject struct obj_layout{ NSUInteger retained; } inline id NSAllocateObject (Class aClass, NSUInteger extraBytes, NSZone *zone){ int size = 計算容納對象所需記憶體大小 id new = NSZoneMalloc(zone,size); memset(new, 0, size); new = (id)& ((struct obj_layout *)new)[1]; } // NSAllocateObject 函數通過調用NSZoneMalloc函數來分配存放對象所需的記憶體空間,之後將該記憶體空間置為0,最後返回作為對象而使用的指針。
註:NSDefaultMallocZone,NSZoneMalloc等名稱中包含的NSZone是什麼呢?它是為了防止記憶體碎片化而引入的結構,對記憶體分配的區域本身進行多重化管理,根據使用對象的目的、對象的大小分配記憶體,從而提高了記憶體管理的效率。
但是,如同蘋果官方文檔中所說,現在的運行時系統只是簡單地忽略的區域的概念,運行時系統中的記憶體管理本身已經極具效率,使用區域來管理記憶體反而會引起記憶體使用效率低下以及源代碼複雜化等問題。
下麵是去掉NSZone後簡化了的源代碼:
GNUstep/modules/core/base/Source/NSObject.m alloc 簡化版 struct obj_layout { NSUInteger retained; } + (id) alloc{ int size = sizeof (struct obj_layout) + 對象大小 struct obj_layout * p = (struct obj_layout *) calloc (1,size); return (id) (p+1) }
alloc類方法用struct obj_layout中的retained整數來保存引用計數,並將其寫入對象記憶體頭部,該對象記憶體塊全部置0後返回。
對象的引用計數可通過retainCount實例方法取得
id obj = [[NSObject alloc] init]; NSLog(@"retainCount = %d",[objc retainCount]); // 顯示retainCount = 1 執行alloc後對象的retainCount 是 “1”,下麵通過GNUstep的源代碼來確認。 - (NSUInteger) retainCount{ return NSExtraRefCount (self) + 1; } inline NSUInteger NSExtraRefCount (id anObject){ return ((struct obj_layout *) anObject) [-1].retained; }
1.2.4 蘋果的實現
在NSObject 類的alloc類方法上設置斷點,追蹤程式的執行,以下列出了執行所調用的方法和函數。
+alloc
+allocWithZone
class_createInstance
calloc
alloc類方法首先調用allocWithZone類方法,這和GNUstep的實現相同,然後調用class_createInstance函數,最後通過調用calloc來分配記憶體塊。這和前面的GNUstep的實現並無多大差異。
retainCount/retain/release實例方法又是怎麼實現的呢?下麵列出各個方法分別調用的方法阿和函數
-retainCount
_CFDoExternRefOperation
CFBasicHashGetCountOfKey
-retain
_CFDoexternRefOperation
CFBasicHashAddValue
-release
_CFDoExternRefOperation
CFBasicHashRemoveValue
(CFBasicHashRemoveValue 返回0 時,-release調用dealloc)
各個方法都通過同一個調用了_CFDoExternRefOperation函數,調用了一系列名稱相似的函數。如這些函數名的首碼"CF"所示,它們包含於Core Foundation框架源代碼中,即是CFRuntime.c的_CFDoExternRefOperation函數。
1.2.5 autorelease
autorelease 就是自動釋放,這看上去很像ARC,但實際上它更類似於C語言中自動變數(局部變數)的特性。
在C語言中,程式執行時,如果某自動變數超出其作用域,該自動變數將被自動廢棄。
{ int a; } // 因為超出變數的作用域,自動變數 int a將被廢棄,不可再訪問。
autorelease會像C語言的自動變數那樣來對待對象實例。當超出其作用域(相當於變數作用域)時,對象實例的release實例方法被調用。另外,同C語言的自動變數不同的是,編程人員可以設定變數的作用域。
autorelease的具體使用方法如下:
(1)生成並持有NSAutoreleasePool 對象。
(2)調用已分配對象的autorelease實例方法
(3)廢棄NSAutoreleasePool對象。
NSAutoreleasePool對象的生存周期相當於C語言變數的作用域。對於所有調用過autorelease實例方法的對象,在廢棄NSAutoreleasePool對象時,都將調用release實例方法。
源代碼如下:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; id obj = [[NSObject alloc] init]; [obj autorelease]; [pool drain] // 這裡的[pool drain] 等同於 “[obj release]”
在Cocoa框架中,相當於程式主迴圈的NSRunLoop或者在其它程式可運行的地方,對NSAutoreleasePool對象進行生成,持有和廢棄處理。因此,應用程式開發者不一定非得使用該對象進行開發工作。
儘管如此,但在大量產生autorelease的對象時,只要不廢棄NSAutoreleasePool對象,那麼生成的對象就不能被釋放,因此有時會產生記憶體不足的現象。典型的例子就是讀取大量圖像的同時改變其尺寸。圖像文件讀入到NSData對象,並從中生成UIImage對象,改變該對象尺寸後生成新的UIImage對象,這種情況下,就會大量產生autorelease的對象。
在此情況下有必要在適當的地方,生成,持有或廢棄Pool對象。
通常在使用OC,也就是Foundation框架時,無論調用哪一個對象的autorelease實例方法,實現上是調用的都是NSObject類的autorelease實例方法。但是對於NSAutoreleasePool類,atorelease實例方法已被該類重載,因此運行時就會出錯。
1.3 ARC規則
1.3.1 概要
實際上“引用計數式”記憶體管理的本質部分在ARC中並沒有改變,就像 “自動引用計數”這個名稱表示的那樣,ARC只是自動的幫助我們處理“引用計數”的相關部分。
對某個文件可選擇使用或不使用ARC.
1.3.2 記憶體管理的思考方式
自己生成的對象,自己持有
非自己生成的對象,自己也能持有
自己持有的對象不再需要時釋放
非自己持有的對象無法釋放
1.3.3 所有權修飾符
OC編程中為了處理對象,可將變數類型定義為id類型或各種對象類型。
所謂對象類型就是指向NSObject這樣的OC類的指針,如 “NSObject *”。id類型用於隱藏對象類型的類名部分,相當於C語言中常用的 “void *”
ARC有效時,id類型和對象類型通C語言其它類型不同,其類型上必須附加所有權修飾符。
__strong __weak __unsafe_unretained __autoreleasing
__strong 修飾符
__strong 修飾符是id類型和對象類型預設的所有權修飾符。也就是說,以下源代碼中的id變數,實際上被附加了所有權修飾符。
id obj = [[NSObject alloc] init];
id 和對象類型在沒有明確指定所有權修飾符時,預設為__strong修飾符。上面的代碼與以下相同。
id __strong obj = [[NSObject alloc] init];
該源代碼再ARC無效的時候又該如何表述呢?
// ARC無效 id obj = [[NSObject alloc] init]; // 該源代碼一看則名,目前在錶面上沒有任何變化,在看看下麵的代碼 { id __strong obj = [[NSObject alloc] init]; } 此源代碼明確指定了C語言的變數的作用於。ARC無效的時候,該源代碼可記述如下: // ARC無效 { id obj = [[NSObject alloc] init]; [obj release]; }
為了釋放生成並持有的對象,增加了調用release方法的代碼。該源代碼進行的動作同先前ARC有效時的動作完全一樣。
如此源代碼所示,附有__strong修飾符的變數obj在超出其變數作用域時,即在該變數被廢棄時,會釋放其被賦予的對象。
如 "strong" 這個名稱所示,__strong 修飾符表示對對象的"強引用"。持有強引用的變數在超出其作用域時被廢棄,隨著強引用的失效,引用的對象會隨之釋放。
下麵關註一下源代碼中關於對象的所有者的部分
{ id __strong obj = [[NSObject alloc] init]; } // 此源代碼就是之前自己生成並持有對象的源代碼,該對象的所有者如下: { // 自己生成並持有對象 id __strong obj = [[NSObject alloc] init] // 因為變數obj為強引用,所以自己持有該對象 } // 因為變數obj超出其作用域,強引用失效,所以自動地釋放自己持有的對象,對象的所有者不存在,因此廢棄該對象。
此處,對象的所有者和對象的生存周期是明確的,那麼,在取得非自己生成並持有的對象時又會如何呢?
// 在NSMutableArray類的array類方法的源代碼中取得非自己生成並持有的對象,具體如下 { // 取得非自己生成並持有的對象 id __strong obj = [NSMutableArray array]; // 因為變數obj為強引用,所以自己持有對象 } // 因為變數obj超出其作用域,強引用失效,所以自動地釋放自己持有的對象 在這裡對象的所有者和對象的生存周期也是明確的 { // 自己生成並持有對象 id __strong obj = [[NSObject alloc] init]; // 因為變數obj為強引用,所以自己持有對象 } // 因為變數obj超出其作用域,強引用失效,所以自動地釋放自己持有的對象,對象的所有者不存在,因此廢棄該對象。
當然,附有__strong 修飾符的變數之間可以相互賦值。
id __strong obj0 = [[NSObject alloc] init]; // 對象A // obj0持有對象A的強引用 id __strong obj1 = [[NSObject alloc] init]; // 對象B // obj1持有對象B的強引用 id __strong obj2 = nil; // obj2不持有任何對象 obj0 = obj1; // obj0持有由obj1賦值的對象B的強引用,因為obj0被賦值,所以原先持有的對對象A的強引用時效,對象A的所有者不存在,因此廢棄對象A. 此時,持有對象B的強引用的變數為 obj0 和 obj1 obj2 = obj0; // obj2 持有obj0賦值的對象B的強引用,此時持有對象B的強引用的變數為obj0,obj1,obj2 obj1 = nil; 此時持有對象B的強引用的變數為obj0,obj2 obj2 = nil; 此時持有對象B的強引用的變數為obj0 obj3 = nil; 此時持有對象B的強引用的變數沒有,所以廢棄對象B
__weak 修飾符
兩個對象迴圈引用的例子。
// ARC下 對象的許可權修飾符是__strong @interface Test : NSObject { id __strong _obj; } - (void)setObject:(id __strong) _obj; @end @implementation Test - (id)init { self = [super init]; return self; } - (void)setObject:(id __strong)obj { _obj = obj; } // 以下為迴圈引用。 { // test0 持有對象a的強引用 id test0 = [[Test alloc] init]; // 對象a // test1 持有對象b的強引用 id test1 = [[Test alloc] init]; // 對象b // 對象a的obj成員變數持有對象b的強引用 [test0 setObject:test1]; // 對象b的obj成員變數持有對象a的強引用 [test1 setObject:test0]; } // 因為test0變數超出其作用域,強引用失效,所以釋放對象a // 因為test1變數超出其作用域,強引用失效,所以釋放對象b // 此時持有對象a的強引用的變數為對象b的obj // 此時持有對象b的強引用的變數為對象a的obj 發生記憶體泄露
// 迴圈引用容易發生記憶體泄露,所謂記憶體泄露就是應當廢棄的對象在超出其生存周期後繼續存在。此代碼的本意是賦予變數test0的對象a和對象b在超出其變數作用域時被釋放,即在對象不被任何變數持有的狀態下予以廢棄。但是,迴圈引用使得對象不能被再次廢棄。
像下麵這種情況,雖然只有一個對象,但在該對象持有其自身時,也會發生迴圈引用(自引用)
id test = [[Test alloc] init];
[test setObject:test];
怎麼樣才能避免迴圈引用呢?看到__strong修飾符就會意識到了,既然有strong,就應該有與之對應的weak,也就是說,使用__weak修飾符可以避免迴圈引用。
__weak修飾符與__strong修飾符相反,提供弱引用,弱引用不能持有對象實例。
id __weak obj = [[NSObject alloc] init]; 變數附加上了 __weak 修飾符,實際上如果編譯以下代碼,編譯器會發出警告 warning: assigning retained obj to weak variable; obj will be released after assignment [-Warc-unsafe-retained-assign]
此源代碼將自己生成並持有的對象賦值給附有__weak修飾符的變數obj,即變數obj持有對象持有對象的弱引用。因此,為了不以自己持有的狀態來保存自己生成並持有的對象,生成的對象會立即被釋放。編譯器對此會給出警告。如果像下麵這樣,將對象賦值給附有__strong 修飾符的變數之後再賦值給附有__weak 修飾符的變數,就不會發生警告了。
{ // 自己生成並持有對象 id __strong obj0 = [[NSObject alloc] init]; // 因為obj0變數為強引用,所以自己持有對象,obj1變數持有生成對象的弱引用 id __ weak obj1 = obj0; } // 因為obj0變數超出其作用域,強引用失效 所以自動釋放自己持有的對象 因為對象的所有者不存在,所以廢棄該對象
因為帶__weak 修飾符的變數(即弱引用)不持有對象,所以在超出其變數作用域時,對象即被釋放如果像下麵這樣將先前可能發生迴圈引用的類成員變數改成附有__weak修飾符的成員變數的話,該現象可以避免。
@interface Test : NSObject { id __weak obj; } - (void)setObject:(id __strong)obj; @end // 互相弱引用 對象a ----> 對象b id __weak obj <---- id __weak obj
__weak修飾符還有另一個優點,在持有某對象的弱引用的時候,若該對象被廢棄,這個弱引用也會被置為nil
id __weak obj0 = nil; { id obj1 = [[NSObject alloc] init]; // 對象a obj0 = obj1; } // 對象a出了作用域被釋放掉,所以弱引用obj0也會被置為nil
像這樣,使用__weak可以避免迴圈引用。
__unsafe_unretained 修飾符
__unsafe_unretained 修飾符正如其名unsafe所示,是不安全的所有權修飾符。儘管ARC式的記憶體管理是編譯器的工作,但附有__unsafe_unretained修飾符的變數不屬於編譯器的記憶體管理的對象,這一點在使用的時候要註意。
id __unsafe_unretained obj = [[NSObject alloc] init]; // 該源代碼將自己生成並持有的對象賦值給附有__unsafe_unretained修飾符的變數中。雖然使用了 unsafe 的變數,但是編譯器不會忽略,而是給出適當的警告。 warning:assignin retained obj to unsafe_unretained variable;obj will be released after assignment
附有 __unsafe_unretained 修飾符的變數同附有__weak 修飾符的變數一樣,因為自己生成並持有的對象不能繼續為自己所有,所以生成的對象會立即被釋放。到這裡,__unsafe_unretained修飾符和__weak修飾符是一樣的,下麵我們看下源代碼的差異:
id __unsafe_unretained obj1 = nil; {
// 自己生成並持有對象,因為obj0變數為強引用,所以自己持有對象 id __strong obj0 = [[NSObject alloc] init];
// 雖然obj0變數賦值給obj1,但是obj1變數既不持有對象的強引用,也不持有弱引用
obj1 = obj0; NSLog(@"A: %@",obj1); }
// 因為obj0變數超出其作用域,強引用失效,所以自動釋放自己持有的對象。因為對象無持有者所以廢棄該對象。
// 輸出obj1 變數表示的對象 obj1變數表示的對象已經被廢棄(懸垂指針) 錯誤訪問 NSLog(@"B: %@",obj1); // 執行的結果為 A : <NSObject :0x753e180> B : <NSObject :0x753e180>
也就是說,最後一行的NSLog只是碰巧正常運行而已。雖然訪問了已經被廢棄的對象,但是應用程式在個別運行狀況下才會崩潰。
再試用__unsafe_unretained修飾符時,賦值給附有__strong修飾符的變數時有必要確保被賦值的對象確實存在。
為什麼需要使用附有__unsafe_unretained修飾符的變數?
比如在iOS4以及 OS X Snow Leopard 的應用程式中,必須使用 __unsafe_unretained修飾符來替代__weak修飾符,賦值給附有 __safe_unretained 修飾符變數的對象在通過該變數使用時,如果沒有確保其確實存在,那麼應用程式就會崩潰。
__autoreleasing 修飾符
ARC有效時不能使用autorelease方法,另外也不能使用NSAutoreleasePool類。這樣一來,雖然autorelease無法直接使用,但實際上ARC有效時autorelease功能是起作用的。
// ARC無效 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; id obj = [[NSObject alloc] init]; [obj autorelease]; [pool drain]; // ARC有效的時候,該源代碼也能寫成下麵這樣 @autoreleasepool{ id __autoreleasing obj = [[NSObject alloc] init]; }
指定“@autoreleasepool塊”來替代“NSAutoreleasePool類對象生成、持有以及廢棄”這一範圍。
另外,ARC有效時,要通過將對象賦值給附加了__autoreleasing 修飾符的變數來替代調用autorelease方法。對象賦值給附有__autoreleasing 修飾符的變數等價於在ARC無效時調用對象的autorelease方法,即對象被註冊到autoreleasepool
可就是說可以理解為,在ARC有效時,用@autoreleasepool塊替代NSAutoreleasePool類,用附有__autoreleasing修飾符的變數替代autorelease方法。
但是,顯示地附加__autoreleasing修飾符同顯式的附加__strong修飾符一樣罕見。
我們通過實例看看為什麼非顯示的使用__autoreleasing修飾符也可以。
取得非自己生成並持有的對象時,如同以下源代碼,雖然可以使用alloc/new/copy/mutableCopy以外的方法來取得對象,但該對象已被註冊到了autoreleasepool。這同在ARC無效時取得調用了autorelease方法的對象是一樣的。這是由於編譯器會檢查方法名是否以alloc/new/copy/mutableCopy開始,如果不是則自動將返回值的對象註冊到autoreleasepool.init方法返回值的對象不會註冊到autoreleasepool。
@autoreleasepool { // 取得非自己生成並持有的對象 id __strong obj = [NSMutableArray array]; // 因為變數obj為強引用,所以自己持有對象,並且該對象由編譯器判斷其方法名後自動註冊到autoreleasepool } // 因為變數obj超出其作用域,強引用失效,所以自動釋放自己持有的對象。 同時,隨著@autoreleasepool塊的結束,註冊到autoreleasepool中的所有對象被自動釋放, 因為對象的所有者不存在,所以廢棄對象
像這樣,不適用__autoreleasing修飾符也能使對象註冊到autoreleasepool.以下為取得非自己生成並持有對象時被調用方法的源代碼實例。
+ (id) array{ return [[NSMutableArray alloc] init]; } // 該代碼沒有使用__autoreleaseing修飾符,可寫成以下形式 + (id) array{ id obj = [[NSMutableArray alloc] init]; return obj; }
因為沒有顯示指定所有權修飾符,所以id obj同附有__strong 修飾符的 id __strong obj是完全一樣的。由於return使得對象變數超出其作用域,所以該強引用對應的自己持有的對象會被自動釋放,但該對象作為函數的返回值,編譯器會自動將其註冊到autoreleasepool。
以下為使用__weak修飾符的例子。雖然__weak修飾符是為了避免迴圈引用而使用的,但在訪問附有__weak修飾符的變數時,實際上必定要訪問註冊到autoreleasepool的對象。
id __weak obj1 = obj0; NSLog(@"class = %@", [obj1 class]); // 以下源代碼與此相同 id __weak obj1 = obj0; id __autoreleasing tmp = obj1; NSL(@"class = %@", [tmp class]);
為什麼在訪問附有__weak 修飾符的變數時必須訪問註冊到autoreleasepool的對象呢?這是因為__weak修飾符只持有對象的弱引用,而在訪問引用對象的過程中,該對象有可能被廢棄。如果把要訪問的對象註冊到autoreleasepool中,那麼在@autoreleasepool塊結束之前都能確保該對象存在。因此,在使用附有__weak修飾符的變數時就必定要使用註冊到autoreleasepool中的對象。
最後一個可非顯示的使用 __autoreleasing修飾符的例子,同前面講述的 id obj 和 id __strong obj 完全一樣嗎。那麼id的指針 id * obj 又如何呢?可以由 id __strong obj 的例子類推出 id __strong *obj 嗎? 其實,推出來的是 id __autoreleasing *obj。同樣的,對象的指針 NSObject **obj 便成為了 NSObject *__autoreleasing * obj;
像這樣,id的指針或對象的指針在沒有顯示指定時會被附加上__autoreleasing修飾符。
比如,為了得到詳細的錯誤信息,經常會在方法的參數中傳遞NSError對象的指針,而不是函數返回值。Cocoa框架中,大多數方法也使用這種方式,如NSString 的 stringWithContentsOfFile:encoding:error類方法等。使用該方式的源代碼如下所示:
NSError *error = nil; BOOL result = [obj performOperationWithError:&error]; 該方法的聲明為: - (BOOL) performOperationWithError:(NSError **)error; 同前面講述的一樣,id的指針或對象的指針會預設附加上__autoreleasing 修飾符,所以等同於以下源代碼 - (BOOL)performOperationWithError:(NSError * __autoreleasing *)error;
參數中持有NSError對象指針的方法,雖然為響應其執行結果,需要生成NSError類對象,但也必須符合記憶體管理的思考方式。
作為alloc/new/copy/mutableCopy方法返回值取得的對象是自己生成並持有的,其它情況下便是取得非自己生成並持有的對象。因此,使用附有__autoreleasing修飾符的變數作為對象取得參數,與除alloc/new/copy/mutableCopy外其他方法的返回值取得對象完全一樣,都會註冊到autoreleasepool,並取得自己生成並持有的對象。
比如performOperationWithError 方法的源代碼就應該是下麵這樣
- (BOOL) performOperationWithError:(NSError * __autoreleasing *)error{ // 錯誤發生 *error = [[NSError alloc] initWithDomain:MyAppDomain code:errorCode userInfo:nil]; return NO; } // 因為聲明為NSError *__autoreleasing *類型的error作為*error被賦值,所以能夠返回註冊到autoreleasepool中的對象
然而,下麵的源代碼會產生編譯器錯誤:
NSError *error = nil; NSError **pError = &error; 賦值給對象指針時,所有權修飾符必須一致。 error:initializing 'NSError *__autoreleasing *' with an expression of type 'NSError *_strong *'changes retain/release properties of pointer 此時,對象指針必須加__strong 修飾符 NSError *error = nil; NSError *__strong *pError = &error; // 編譯正常 當然,對於其他所有權修飾符也是一樣 NSError __weak *error = nil; NSError * __weak *pError = &error; // 編譯正常 NSError __unsafe_unretained *unsafeError = nil; NSError * __unsafe_unretained *pUnsafeError = &unsafeError; // 編譯正常
前面的方法參數中使用了附有__autoreleasing 修飾符的對象指針類型。
- (BOOL) performOperationWithError:(NSError * __autoreleasing *)error;
然而調用方卻使用了附有__strong 修飾符的對象指針類型;
NSError __strong *error = nil; BOOL result = [obj performOperationWithError:&error];
對象指針型賦值時,其所有權修飾符必須一致,但為什麼該源代碼沒有警告就順利通過編譯了呢?實際上,編譯器自動地將該源代碼轉化成了下麵的形式。
NSError __strong *error = nil; NSError __autoreleasing *tmp = error; BOOL result = [obj performOperationWithError:&tmp]; error = tmp;
當然也可以顯示地指定方法參數中對象指針類型的所有權修飾符
- (BOOL) performOperationWithError:(NSError * __strong *)error;
像該源代碼的聲明一樣,對象不註冊到autoreleasepool也能夠傳遞。但是前面也說過,只有作為alloc/new/copy/mutableCopy 方法的返回值而取得對象時,能夠自己生成並持有對象。其他情況即為“取得非自己生成並持有的對象”,這些務必牢記。為了在使用參數取得對象時,貫徹記憶體管理的思考方式,我們要將參數聲明為附有__autoreleasing修飾符,但在顯示的指定 __autoreleasing 修飾符時,必須註意對象變數要為自動變數(包括局部變數、函數以及方法參數)。
下麵我們換個話題,詳細瞭解一下@autoreleasepool.如以下源代碼所示,ARC無效時,可以將NSAutoreleasePool對象嵌套使用。
// ARC無效 NSAutoreleasePool *pool0 = [[NSAutoreleasePool alloc] init]; NSAutoreleasePool *pool1 = [[NSAutoreleasePool alloc] init]; NSAutoreleasePool *pool2 = [[NSAutoreleasePool alloc] init]; id obj = [[NSObject alloc] init]; [obj autorelease]; [pool2 drain]; [pool1 drain]; [pool0 drain]; 同樣的,@autoreleasepool 也能夠嵌套使用 @autoreleasepool{ @autoreleasepool{ @autoreleasepool{ id __autoreleasing obj = [[NSObject alloc] init]; } } }
比如,在iOS應用程式模板中,像下麵的main函數一樣,@autoreleasepool塊包含了全部程式。
int main(int argc, char *argv[]){ @autoreleasepool{ return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
NSRunLoop等實現不論ARC有效還是無效,均能夠隨時釋放註冊到autoreleasepool中的對象。
另外,如果編譯器版本為LLVM3.0以上,即使ARC無效@autoreleasepool塊也能夠使用,如下:
// ARC無效 @autoreleasepool{ id obj = [[NSObject alloc] init]; [obj autorelease]; }
因為autoreleasepool 範圍以塊級源代碼表示,提高了程式的可讀性,所以今後在ARC無效時也推薦使用 @autoreleasepool塊。
另外,無論ARC是否有效,調試用的非公開函數_objc_autoreleasePoolPrint()都可以使用
_objc_autoreleasePoolPrint()
利用這一函數可有效的幫助我們調試註冊到autoreleasepool 上的對象。
1.3.4 規則
- 不能使用 retain/realese/retainCOunt/autorelease
- 不能使用 NSAllocateObject/NSDeallcateObject
- 必須遵守記憶體管理的方法命名規則
- 不要顯示調用dealloc
- 使用@autoreleasepool代替 NSAutoreleasePool
- 不能使用區域 (NSZone)
- 對象型變數不能作為C語言結構體的成員
- 顯示轉化 id 和 void *
在ARC無效時,像一下代碼這樣將id變數強制轉換成 void * 變數並不會出問題。
// ARC無效 id obj = [[NSObject alloc] init]; void *p = obj; 更近非同步,將該void * 變數賦值給 id 變數中,調用其實例方法,運行時也不會有問題。 // ARC無效 id o = p; [o release]; // 但是在ARC有效時這便會引起編譯錯誤 error : implicit coversion of an OC pointer to 'void *' is disallowed with ARC
id 型或對象型變數賦值給 void * 或者逆向賦值時都需要進行特定的轉換。如果只想單純的賦值,則可以使用 "__bridge 轉換"
id obj = [[NSObject alloc] init]; void *p = (__bridge void *)obj; id o = (__bridge id)p
像這樣,通過 “__bridge轉換”, id 和 void * 就能夠相互轉換。
但是轉換為 void 的 __bridge 轉換,其安全性與賦值給 __unsafe_unretained 修飾符相近, 甚至會更低。如果管理時不註意賦值對象的所有者,就會因懸垂指針導致程式崩潰。
__bridge 轉換中還有另外兩種轉換,分別是 “__bridge_retained 轉換” 和 “__bridge_transfer” 轉換。
id obj = [[NSObject alloc] init]; void *p = (__bridge_retained void *)obj
__bridge_retained 轉換可使要轉換賦值的變數也持有所賦值的對象。 。。。。。
1.3.5 屬性
當ARC有效時,oc類的屬性也會發生變化
@property (strong ,nonatomic) NSString *name;
當ARC有效時,以下可作為這種屬性聲明中使用的屬性來用。
屬性聲明的屬性 | 所有權修飾符 |
assign | __unsafe_unretained修飾符 |
copy | __strong 修飾符(但是賦值的是被賦值的對象) |
retain | __strong 修飾符 |
strong | __strong 修飾符 |
unsafe_unretained | __unsafe_unretained 修飾符 |
week | __weak 修飾符 |
以上各種屬性賦值給指定的屬性中就相當於賦值給附加各屬性對應的所有權修飾符的變數中,只有copy屬性不是簡單的賦值,它賦值的是通過NSCopying 結構的 copyWithZone 方法複製賦值源所生成的對象。
另外,在聲明類成員變數時,如果同屬性聲明中的屬性不一致則會引起編譯錯誤。比如下麵的這種情況,
id obj
在聲明 id obj 成員變數時,像下麵這樣,定義其屬性為weak
編譯器會報錯。
此時,成員變數的聲明中需要附加 __weak 修飾符。
id __weak obj;
或者使用 strong 屬性替代 weak 屬性
@property (nonatomic, strong ) id obj