自動引用計數(Automatic Reference Counting, ARC)把壓在程序員們肩頭的管理內存的重擔卸除了不少,更不用說讓跟蹤內存洩漏那樣的煩心事也少了很多。不過,雖然ARC很棒,我們仍然不能完全把內存管理這回事兒拋在腦後。
這篇文章將要討論以下方面的問題,幫助大家快速進入ARC的世界。
在ARC出現以前,程序員們只能靠retain/relese/autorelease來確保對象們恰好“堅持”到被需要的那一刻。如果忘了retain,或者多次release某個對象,程序就會發生內存洩漏的問題,甚至直接崩潰。
在Xcode 4.2中,除了語法檢查外,Apple的新LLVM編譯器還將內存管理的苦差事接了過來,它會檢查代碼,決定何時釋放對象。Apple的文檔裡是這麼定義ARC的:
“自動引用計數(ARC)是一個編譯器級的功能,它能簡化Cocoa應用中對象生命周期管理(內存管理)的流程。”
ARC使內存管理在大部分時候變得如同小事一樁,但我們仍要在決定自己的類如何管理其它對象的引用時承擔一些責任。
那麼,讓我們正式開始吧……
引用計數: 快速復習
手工管理、引用計數式的內存管理在iOS中是這樣工作的: 當使用alloc/init(或其它類似方法)創建對象時,隨同對象返回的,還有個retainCount,其值為1,表明我們獲得了這個對象的所有權。
NSObject *obj = [[NSObject alloc] init]; // do some stuff [obj release];
在對象的alloc/init和release(即放棄對象的所有權)之間,我們可以任意處理它,這很安全,因為系統是無法回收正在使用中的對象的。
將對象加入到自動釋放池也是類似,對象會一直存在,直到未來的某個時間我們不再需要它,才會被系統回收。
-(NSObject*) someMethod { NSObject *obj = [[[NSObject alloc] init] autorelease]; return obj; // will be deallocated by autorelease pool later }
ARC的工作原理
大多數新的iOS程序員都會在引用計數這問題上遇到理解障礙。ARC則是一個編譯前的步驟,它為我們的代碼自動加上retain/release/autorelease語句。
ARC並不是垃圾收集,而且,引用計數也沒有消失,只是變成自動而已。聽起來像是事後追加的這麼一個功能,不過,只要我們想一想Objective-C有多少功能是通過對源文件的預處理來實現的,就不會這麼想了。
當采用ARC後,代碼只要這樣寫:
NSObject *obj = [[NSObject alloc] init]; // do some stuff
ARC會自動將它變成:
NSObject *obj = [[NSObject alloc] init]; // do some stuff [obj release]; // **Added by ARC**
從下圖(來自Apple官方文檔)看起來,好像retain/release的數量快趕上真正有用的代碼了。當然,這肯定不是熟手的情況,不過可以看成是對新手的保守估計。這些代碼跑起來,要跟蹤某個內存問題真的是會搞死人。
在工程中開啟ARC
如果想開啟ARC,只要在工程的Build Settings中設置ARC為YES。在幕後,實際上是設置了-fobj-arc的編譯器標識。
ARC施加的新規則
如果想用ARC,必須服從一些新規則。
1. 對象的Alloc/Init
創建對象的方法跟以前一樣,但你一定不能調用retain/release/autorelease/retainCount。也不能通過selector偷偷地調用它們: 禁止使用@selector(retain)和@selector(release)。
2. dealloc方法
ARC為自動為你調用,一定不能直接調用dealloc。不過,如果你需要釋放實例變量以外的資源,還是可以創建自定義的dealloc方法。但在這個方法裡,不要調用[super dealloc]。因為ARC會幫你調。
3. 聲明的屬性
在ARC之前,我們是用@property指令中的assign/retain/copy參數來告訴編譯器,如何管理這些屬性的內存。用了ARC之後,這些參數就作廢了,改用weak/strong這兩個參數。
4. C結構中的對象指針
同樣禁止使用。文檔裡建議不要把它們放在結構了,改放到類裡去。否則ARC就不認識它們了。可能會出現一些移植上的問題。不過,ARC是可以以文件為單位來關閉的。參考下文的“引入不兼容ARC的代碼”。
5. id與void*之間的臨時互轉
當我們在Core Foundation的C函數和Foundation Kit的Objective-C方法間傳遞對象時,常常需要進行id和void*兩個類型的互轉。叫做無縫橋接(Toll Free Bridging)。
如果使用ARC,必須在CF對象進入和脫離ARC的控制時,用提示/限定符來告知編譯器。限定符有__bridge、__bridge_retain和__bridge_transfer。另外,仍需要用CFRetain和CFRelease來管理Core Foundation的對象。
這一塊已經比較高深了,如果你不清楚CF對象是什麼,也不需要太煩惱。
6. 以@autoreleasepool代替NSAutoReleasePool
兼容ARC的代碼不能再使用NSAutoReleasePool對象,而要改用@autoreleasepool{}塊。一個很好的例子:
int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([ExampleAppDelegate class])); } }
7. 其它
基於Zone的內存已經沒了(在運行時裡也沒了)。不能再使用NSAllocateObject和NSDeallocateObject。
ARC限定符 - 聲明的屬性
身為程序員,習慣於做出一些決定,例如把某個量聲明為變量還是常量、本地還是全局,等等。因此,在這裡,我們也要決定某個屬性與其它屬性的關系。我們用strong/weak來把這一關系告訴編譯器。
強引用
強引用是對某對象的引用,並且能阻止它被回收。換句話說,強引用創建了一個所有關系。在ARC之前,我們這麼寫:
// Non-ARC Compliant Declaration @property(retain) NSObject *obj;
在ARC下,我們需要這麼寫,以確保當前實例獲得被引用對象的所有權(主人不被回收,它也不能被回收)。
// ARC Compliant Declaration @property(strong) NSObject *obj;
弱引用
弱引用是對某對象的引用,但不能阻止它被回收。換句話說,弱引用並不會創建所有關系。在ARC之前,我們這麼寫:
// Non-ARC Compliant Declaration @property(assign) NSObject *parentObj;
在ARC下,我們需要這麼寫,以確保當前實例沒有獲得被引用對象的所有權(一般來說,子對象不應該擁有父對象,這時可以用弱引用)。
// ARC Compliant Declaration @property(weak) NSObject *parentObj;
ARC限定符 - 常規變量
上一節是說明如何管理屬性。對於常規變量,則有:
__strong __weak __unsafe_unretained __autoreleasing
一般來說,我們不太需要使用上面這些限定符。在使用移植工具的時候可能會看到那麼幾個,但新工程基本上不需要。
__strong: 默認限定符,不需要顯式指定。表示任何用alloc/init創建的對象在當前范圍的生命期內得以保留。“當前范圍”是指變量聲明語句所在的兩個大括號之間(方法、循環、塊,等等)。
__weak: 表示對象可以隨時被摧毀。只有當它被其它對象強引用時才有用。__weak變量在摧毀時,被設為nil。
__unsafe_unretained: 與__weak類似,但在摧毀時,不設為nil,保留原值(不再指向有效的東西)。
__autoreleasing: 不要與autorelease搞混,它用於通過引用傳遞對象,比如,通過引用傳遞NSError對象: [myObject performOperationWithError:&tmp]。
來源: Clang project
注: 我們發現在ARC下,@property中使用“retain”時(而不是“strong”),編譯器並不會報錯,而且能生成同樣結果。但以後可能會變,所以還是用“strong”吧。
移植到ARC
Xcode 4.2提供了一個移植ARC的工具,還可以幫你將無法自動移植的代碼手工轉換過去。
打開不兼容ARC的工程,進入Edit > Refactor > Convert to Objective-C ARC。
2. 選擇需要轉換的構建目標和文件(在後續步驟排除不需要轉換的文件)
3. 運行預查,按下一步。
注: 按下一步後,LLVM編譯器會開始構建,以便對工程進行分析。如果有錯誤,是無法進入下一步的。如果是第一次打開低版本Xcode建立的工程,請先執行清理。
4. 檢查一下工具建議的修改,並選擇是否要排除某個文件。然後按下保存。
注: 如果有文件無法移植,工具也會告訴你。並不是所有文件都需要移植(包括庫)。ARC是以文件為基礎生效的,可以參考下文,看編譯時如何把不需要開啟ARC的文件排除在外。
5. 工具會自動設置編譯器的標識,開啟ARC。可以查看工程的Build Settings確認這一點。
引入不兼容ARC的代碼
Apple的文檔說,“ARC能以文件為基礎,與采用手工引用計數的代碼進行交互。你可以在部分文件上使用手工引用計數。”
它的意思是說,我們可以讓一部分文件用ARC,另一部分文件不用。下面是將文件批量排除在ARC之外的步驟。在我寫下這篇文章的時候,還有許多流行的庫都沒有用ARC,為了解決這個問題,請按照下面的步驟做:
在Xcode的工程樹上,點擊你自己的工程
點擊Target
選擇Build Phases標簽
展開Compile Sources
選擇需要排除在ARC外的文件
按下回車
輸入-fno-objc-arc
再按下回車
現在,你選中的文件都有了-fno-objc-arc編譯器標識,會被排除在ARC之外
我該用ARC嗎?
如果你是Objective-C的新手,肯定會歡迎ARC。一開始,有太多的東西要學習,有了ARC就不用擔心手工計數的問題。隨著你開始接觸一些已有的庫,就會經歷一些痛苦(譯者注: 目前的第三方庫很多不兼容ARC),但只要習慣了將它們排除在ARC之外,就沒什麼問題了。
如果你不是新手,在沒有ARC的年代已經玩的很high,那麼可能會覺得“我干嘛要用它!”對你來說,這可能是正確的答案——就目前而言。因為,大多數流行的庫都還沒轉到ARC下,而且ARC對Core Foundation的支持也不好。使用CF類的時候有一些限制,而且,移植代碼的時候,為了讓免費橋接生效,還需要加上一大堆限定符。
在我看來,目前ARC已經是可以使用的狀態了。不過,除非你對它很熟悉,否則還是先用在新工程裡會比較好。雖然ARC兼容iOS 4.0以上,但弱引用只在iOS 5.0以上才支持,所以現在還是不要把所有東西都移植過去(有一些相關的文章,請參考最後的“資源”一節)
至於性能方面,早前有報告指出用ARC之後速度會變快,我想可能是由於減少對自動釋放的依賴的緣故。雖然這完全可以通過改進代碼、更好地使用retain/release來實現,但我覺得重點在於,ARC總是會自動幫你選擇最優的方法。
目前為止,還是有很多令人苦惱的東西被移到ARC,但還不是全部。在長周末後我們會離開一段時間以投入到新的項目,但是這是蘋果一個“新的”推薦手段,所以你可以肯定以後的設計決策會繼續強大ARC並且讓大家離開使用手動的引用計數。
(來自:開源中國AlfredCheung、周榮冰的翻譯)