iOS開發之深入淺出 (7) — ARC總結

通過前面幾篇文章的介紹,我想大家應該對ARC有了一個比較完整的理解。最後,我們來對ARC做一個總結,並把一些未涉及到的細節部分再深入討論一下。

內存管理基本原則

內存管理的依循下面的基本原則

  • 自己生成的對象,那麼既是其持有者
  • 不是自己生成的對象,也可成爲其持有者(一個對象可以被多個人持有)
  • 如果不想持有對象的時候,必須釋放其所有權
  • 不能釋放已不再持有所有權的對象不管ARC有沒有效,該原則始終存在。

所有權關鍵字

從代碼上看,有ARC的代碼和沒有ARC的代碼區別就在下面的幾個關鍵字。

類似 NSObject* 的對象類型,或者 id 類型1,當ARC有效的時候,根據具體情況,這些關鍵字必須要使用2

  • __strong
  • __weak
  • __unsafe_unretained
  • __autoreleasing__strong是默認的修飾符。__weak修飾了一個自動nil的weak引用。 __unsafe_unretained聲明瞭一個不會自動nil的weak引用。當變量被釋放,那麼它就變成了一個野指針了。 __autoreleasing 用來修飾一個聲明爲 (id *) 的函數的參數,當函數返回值時被釋放。

接下來,我們結合下面ARC的使用準則,來看看一些使用ARC後的技術細節。

ARC使用準則

爲了比秒程序秒退的尷尬,ARC有效時,我們的代碼必須遵循下面的準則。

  • 不能使用 retain/release/retainCount/autorelease
  • 不能使用 NSAllocateObject/NSDeallocateObject
  • 不能使用 NSZone
  • 不能明示調用dealloc
  • 內存管理相關的函數必須遵循命名規則
  • 使用@autoreleasepool代替NSAutoreleasePool
  • Objective-C 對象不能作爲C語言結構體(struct/union)的成員
  • 【id】與【void*】之間需要明示cast

建議使用Objective-C的class來管理數據格式,來代替C語言的struct。不能隱式轉換 id 和 void *。

讓我們一個一個來分析

不能使用 retain/release/retainCount/autorelease

內存管理完全交給編譯器去做,所以之前內存相關的函數(retain/release/retainCount/autorelease)不能出現在程序中。Apple的ARC文檔中也有下面的說明。

ARC 有效後,不需要再次使用retain 和 release

如果我們在程序中使用這些函數,經得到類似下面的編譯錯誤信息。

    error: ARC forbids explicit message send of ’release’          [o release];           ^ ~~~~~~~

不能使用 NSAllocateObject/NSDeallocateObject

生成並持有一個Objective-C對象的時候,往往像下面一樣使用NSObject的alloc接口函數。

    id obj = [NSObject alloc];

實際上,如果我們看了GNUstep 中關於 alloc 的代碼就會明白,實際他是使用 NSAllocateObject 來生成並持有對象實例的。換言之,ARC有效的時候,NSAllocateObject函數的調用也是禁止的。如果使用,也會遇到下面的編譯錯誤。

    error: ’NSAllocateObject’ is unavailable:         not available in automatic reference counting mode

同樣,對象釋放時使用的 NSDeallocateObject 函數也不能使用。

不能使用 NSZone

NSZone 是什麼?NSZone 是爲了防止內存碎片而導入的一項措施。Zone 是內存管理的基本單元,系統中管理複數的Zone。系統根據對象的使用目的,尺寸,分配其所屬的Zone區域。以提高對象的訪問效率,避免不必要的內存碎 片。但是,現在的運行時系統(用編譯開關 __OBJC2__ 指定的情況下)是不支持Zone概念的。所以,不管ARC是否有效,都不能使用 NSZone。

不能明示調用dealloc

不管是否使用ARC,當對象被釋放的時候,對象的dealloc函數被調用(就像是C++中對象的析構函數)。在該函數中,需要做一些內存釋放的動作。比如,當對象中使用了malloc分配的C語言內存空間,那麼dealloc中就需要像下面一樣處理內存的釋放。

- (void) dealloc {     free(buffer_); }

又或者是註冊的delegate對象,觀察者對象需要被刪除的時候,也是在dealloc函數中動作。

- (void) dealloc {     [[NSNotificationCenter defaultCenter] removeObserver:self]; }

如果在ARC無效的時候,我們還要像下面一樣,調用父類對象的dealloc函數。

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

但是當ARC有效的時候,[super dealloc];的調用已經被編譯器自動執行,已經不需要我們明示調用了。如果你在代碼中還這樣寫,難免遇到下面的錯誤。

    error: ARC forbids explicit message send of ’dealloc’          [super dealloc];           ^ ~~~~~~~

內存管理相關的函數必須遵循命名規則

在iPhone開發之深入淺出 (3) — ARC之前世今生中,我們知道如果是 alloc/new/copy/mutableCopy/init 開頭的函數,需要將對象所有權返回給調用端。這條規則不管ARC是否有效都應該被遵守。只是 init 開頭的函數比較特殊,他只在ARC下有要求,而且異常苛刻。

init 開始的函數只能返回id型,或者是該函數所屬的類/父類的對象類型。基本上來說,init函數是針對alloc函數的返回值,做一些初始化處理,然後再將該對象返回。比如:

    id obj = [[NSObject alloc] init];

再比如下面定義的函數就是不對的:

    - (void) initThisObject;

需要是下面這樣:

    - (id) initWithObject:(id)obj;

另外,下面名爲 initialize 的函數比較特殊,編譯器將把它過濾掉,不按上面的規則處理。

使用@autoreleasepool代替NSAutoreleasePool

在ARC之下,已經不能在代碼中使用 NSAutoreleasePool,我們之前寫 main.m 文件的時候,往往像下面這樣寫。

int main(int argc, char *argv[]) {     
	NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];     
	int retVal = UIApplicationMain(argc, argv, nil, nil);     
	[pool release];     
	return retVal; 
}

而當ARC有效後,我們需要用@autoreleasepool代替NSAutoreleasePool。

int main(int argc, char *argv[]) {     
	@autoreleasepool {         
	return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));     
	
}

當編譯器看到 @autoreleasepool 定義的塊後會自動生成 NSAutoreleasePool 對象,並將需要的對象放入 AutoReleasePool 中,當出方塊的定義範圍時,pool 中的對象將被釋放。

Objective-C 對象不能作爲C語言結構體(struct/union)的成員

當我們設置ARC有效,並在C語言的結構體中定義Objective-C的對象時,將出現類似下面的編譯錯誤。

struct Data {    
	 NSMutableArray *array; 
};
    error: ARC forbids Objective-C objs in structs or unions          NSMutableArray *array;                          ^

由於 ARC 是將內存管理的細節委託給編譯器來做,所以說編譯器必須要管理對象的生命週期。而LLVM 3.0中不存在對單純C語言構造體成員的內存管理方法。如果單純是棧對象,利用進出棧原理,可以簡單地維護對象的生命週期;而結構體是不行的,簡單地理 解,結構體沒有析構函數,編譯器自身不能自動釋放其內部的 Objective-C 對象。

當我們必須在C語言的結構體中放入 Objective-C 對象的時候,可以使用 void* 轉型,或者使用 __unsafe_unretained 關鍵字。比如下面:

struct Data {     
	NSMutableArray __unsafe_unretained *array; 
};

這樣一來,該內存信息不在編譯器內存管理對象內,僅僅是使用而已,沒有對象的持有權。當然,對象所有權的持有者需要明確的管理他與該結構體的交互,不要引起不必要的錯誤3

【id】與【void*】之間需要明示cast

ARC 有效的時候,由於編譯器幫我們做了內存管理的工作,所以我們不需要太擔心。但是當與 ARC 管理以外的對象類型交互的時候,就需要特殊的轉型關鍵字,來決定所有權的歸屬問題。

主要的轉型關鍵字是:

關鍵字 解釋
__bridge 單純的類型轉換,沒有進行所有權的轉移
__bridge_retained 類型轉換是伴隨所有權傳遞,轉換前後變量都持有對象的所有權
__bridge_transfer 類型轉換伴隨所有權轉移,被轉換變量將失去對象的所有權

當我們在 Core Foundation 對象類型與 Objective-C 對象類型之間切換的時候,需要把握下面的因素:

  • 明確被轉換類型是否是 ARC 管理的對象 
    • Core Foundation 對象類型不在 ARC 管理範疇內
    • Cocoa Framework::Foundation 對象類型(即一般使用到的Objectie-C對象類型)在 ARC 的管理範疇內
  • 如果不在 ARC 管理範疇內的對象,那麼要清楚 release 的責任應該是誰
  • 各種對象的生命週期是怎樣的

題外話

Xcode 4.3帶來的變化

最近隨着 iOS 5.1 的推出,Xcode也推出了4.3版本。在該版本下,ARC 有效時的屬性(@property) 定義的時候,如果不明確指定所有權關鍵字,那麼缺省的就是 strong。而在 Xcode4.2 中,即使 strong 也要顯示指定。

在 Xcode4.2 的時候,針對下面的代碼,

// ARC 無效
 @property (nonatomic, retain) NSString *string;  // --->  
// ARC 有效 
@property (nonatomic, strong) NSString *string;

而在 Xcode 4.3 中,我們可以這麼做,

// ARC 無效 
@property (nonatomic, retain) NSString *string;  // --->  
// ARC 有效 
@property (nonatomic) NSString *string;

ARC 代碼自動變換

另外,Xcode 4.2開始,增加了舊代碼向 ARC 代碼自動轉換的功能。有興趣的朋友可以試試。位置是:

Edit->Refactor->Convert to Objective-C ARC…

爲什麼iOS中沒有GC

我們已經知道ARC並不是GC(垃圾回收)了,那麼,爲什麼iOS中不支持該機能呢?還特意搞出個ARC來。以下是我的分析:

  • 消耗CPU時間的處理儘量避免,以節約電池電量
  • GC執行的後,會停掉運行時庫;這是最大的心結
  • 嵌入式設備本身內存就不是很大,如果GC不停的在後臺運行,執行的頻率會很高,嚴重影響性能 
    • UI動畫處理是iOS的一大賣點,而有了GC後可能會引起不必要的性能損失

1. 關於Objective-C對象的解釋,可以參考iPhone開發入門(7)— 從C/C++語言到Objective-C語言。

2. 當然,如果你不寫,編譯器會用缺省的值代替。具體見iPhone開發之深入淺出 (3) — ARC之前世今生中的描述。

3. 關於這一點,可以參考iPhone開發之深入淺出 (1) — ARC是什麼 一文,明白爲什麼 __unsafe_unretained 是危險的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章