在ARC之前,iOS內存管理無論對資深級還是菜鳥級開發者來說都是一件很頭疼的事。我參加過幾個使用手動內存管理的項目,印象最深刻的是一個地圖類應用,由於應用本身就非常耗內存,當時為了解決內存洩露問題,每周都安排有人值班用Instruments挨個跑功能,關鍵是每次都總能檢查出來不少。其實不管是菜鳥級還是資深級開發者都避免不了寫出內存洩露的代碼,規則大家都懂,可是天知道什麼時候手一抖就少寫了個release?
好在項目決定轉成ARC了,下面將自己轉換的過程和中間遇到的問題寫出來和大家共享,希望能減少大家解決同類問題的時間。
一、前言
項目簡介
需要轉換的Objective-C文件數量:1000個左右。
開發工具:Xcode 6.0.1
轉換方式
我使用的是Xcode本身提供的ARC轉換功能。當然你也可以手動手動轉換,那不屬於本文范疇,而且其工作量絕對能讓你崩潰。
二、轉換過程
代碼備份
在進行如此大規模的更改之前,一定要先進行代碼備份:直接在本地將代碼復制一份,或者記住更改前代碼在VCS上的版本號。
過濾無需轉換的文件
找出項目中引用的仍使用手動內存管理的第三方庫,或者某些你不希望轉換的文件,對其添加-fno-objc-arc標記。
Xcode自動轉換工具只針對Objective-C對象,只會處理Objective-C/Objective-C++即後綴名為.m/.mm的兩種文件,因此其他的C/C++對應的.c/.cpp都無需理會。
執行檢查操作
使用Xcode轉換工具入口如圖所示:
點擊Convert to Objective-C ARC後會進入檢查操作入口,如圖:
該步驟要選擇哪些文件需要轉換,如果前面將無需轉換的文件都添加了-fno-objc-arc標記後,這裡可以全選。
點擊check按鈕後Xcode會幫助我們檢查代碼中存在的不符合ARC使用規則的錯誤或警告,只有所有的錯誤都解決以後才能執行真正的轉換操作。
解決錯誤/告警
執行完check操作後,會給出提示:
三百多個錯誤,同時還有一千兩百多個警告信息,都要哭了。。。
錯誤和警告的解決內容較多,後面會單獨介紹。
執行轉換操作
解決完所有的error後,會彈出下述提示界面:
大意是Xcode將要將你的工程轉換成使用ARC管理內存,所有更改的代碼在真正更改之前會在一個review界面展示。同時所有的更改完成以後,Xcode會講項目Target對應的工程設置的使用ARC設置(Objective-C Automatic Reference Counting)會被置成YES(上圖右上角的警告標識就是在告訴我們項目已經支持ARC了,但工程中有文件還不支持):
這時離成功就不遠了,勝利在望!
點擊next按鈕後跳轉到review界面,樣式類似於使用Xcode提交SVN的確認提交界面,如下圖所示:
該界面列出了所有需要有代碼更改的文件,同時能夠直接對比轉換前和轉換後的代碼變化。為了穩妥起見,我選擇了每個文件都點進去掃了一眼,這也給我們一次機會檢查是否漏掉了不能轉換的文件。確定一切無誤以後,點擊右下角的save按鈕,一切就大功告成了!
錯誤/警告解決
錯誤
ARC forbids synthesizing a property of an Objective-C object with unspecified ownership or storage attribute
property屬性必須指定一個內存管理關鍵字,在屬性定義處增加strong關鍵字即可。
ARC forbids explicit message send of ‘release’
這種情況通常是使用包含release的宏定義,將該宏和使用該宏的地方刪除即可。
Init methods must return a type related to the receiver type
錯誤原因是A類裡的一個方法以init開頭,而且返回的是B類型,好吧,乖乖改方法名。
Cast of C pointer type ‘ivPointer’ (aka ‘void ’) to Objective-C pointer type ‘iFlyTTSManager_old ’ requires a bridged cast
cast_pointer_objective-c
這是Toll-Free Bridging轉換問題,在ARC下根據情況使用對應的轉換關鍵字就行了,後文會專門介紹。
警告
解決警告的目的是消除警告處代碼存在的隱患,既然Xcode給了提示,那麼每一個警告信息都值得我們認真對待。
Capturing self in this block is likely to lead to a retain cycle
這是典型的block循環引用問題,將block中的self改成使用指向self的weak指針即可。
Using ‘initWithArray:’ with a literal is redundant
好吧,原來是沒必要的alloc操作,直接按Xcode提示將alloc刪除即可:
Init methods must return a type related to the receiver type
原來是A類裡的一個方法以init開頭,而且返回的是B類型,好吧,乖乖改方法名。
Property follows Cocoa naming convention for returning ‘owned’ objects
這是因為@property屬性的命名以new開頭了,可惡。。。修改方法是將對應的getter方法改成非new開頭命名的:
ARC下方法名如果是以new/alloc/init等開頭的,而且還不是類的初始化方法,就該小心了,要麼報錯,要麼警告,原因你懂的。
Block implicitly retains ‘self’; explicitly mention ‘self’ to indicate this is intended behavior
意思是block中使用了self的實例變量_selectedModeMarkerView,因此block會隱式的retain住self。Xcode認為這可能會給開發者造成困惑,或者因此而因襲循環引用,所以警告我們要顯示的在block中使用self,以達到block顯示retain住self的目的。
該警告有兩種改法: ①按照Xcode提示,改成self->_selectedModeMarkerView:
②直接將該警告關閉 警告名稱為:Implicit retain of ‘self’ within blocks 對應的Clang關鍵字是:-Wimplicit-retain-self
Weak property may be unpredictably set to nil 和 Weak property ‘delegate’ is accessed multiple times in this method but may be unpredictably set to nil; assign to a strong variable to keep the object alive
這是工程中數目最多的警告,這是因為所有的delegate屬性都是weak的,Xcode默認開啟了下圖中的兩個警告設置,將其關閉即可:
Capturing ‘self’ strongly in this block is likely to lead to a retain cycle
這是明顯的block導致循環引用內存洩露的情況,之前代碼中坑啊!修改方案:
Method parameter of type ‘NSError __autoreleasing ’ with no explicit ownership
這種就不用說了,按警告中的提示添加__autoreleasing關鍵字即可。
以上列出的錯誤和警告只是數量較多的,還有很多其他就不在這裡一一列舉了。
另外,推薦 Mattt Thompson 大神關於Clang中幾乎所有warning的名稱和對應報錯提示語的網站:http://fuckingclangwarnings.com/,以後解決warning類問題就簡單多了!
Xcode自動轉換
關鍵字轉換
Xcode會自動將某些關鍵字自動轉換成ARC的對應版本。
retain自動轉成strong,如圖:
assign關鍵字轉成weak
修飾Objective-C對象或者id類型對象的assign關鍵字會被轉成weak,如圖:
但是修飾Int/bool等數值型變量的assign不會自動轉換成weak,如圖:
關鍵字刪除
和手動內存管理相關的幾個關鍵字,比如:release/retain/autorelease/super dealloc等會被刪除;
dealloc方法中如果除了release/super dealloc語句外,如果別的代碼,dealloc方法會保留,如圖:
如果沒有整個方法都會被刪除:
關鍵字替換
在轉換時block關鍵字會被自動替換成weak:
@autoreleasepool
NSAutoreleasePool不支持ARC,會被替換成@autoreleasepool:
關於被宏注釋代碼
使用宏定義的對象釋放代碼
宏定義如下所示:
#define RELEASE_SAFELY(__POINTER) { \ [(__POINTER) release]; (__POINTER) = nil; }
在執行ARC轉換檢查操作時,Xcode會在使用該宏的地方報錯:
將該宏和使用該宏的地方刪除即可。
被宏注釋掉的代碼,Xcode在轉換時是不會處理的,如圖:
PS:這是相當坑的一點,因為你根本預料不到工程中使用了多少宏,注釋掉了多少代碼。當你執行完轉換操作,以為就大功告成的時候,卻在某天因為一個宏的開啟遇到了一堆新的轉ARC不徹底的問題。這種問題也沒招,只能遇到一個改一個了。
ARC和block
不管是手動內存管理還是ARC,block循環引用導致的內存洩露都是一個令人頭疼的問題。在MRC中,解決block循環引用只需要使用__block關鍵字,在ARC下解決與block的使用就略顯復雜了:
__block關鍵字
block內修改外部定義變量
和手動內存管理一樣,ARC如果在block中需要修改block之外定義的變量需要使用__block關鍵字修飾,比如:
__block NSString *name = @"foggry"; self.expireCostLabel.completionBlock = ^(){ name = @"wangzz"; };
上例中name變量需要在block中修改,因此必須使用__block關鍵字。
__block在MRC和ARC中的區別
在ARC下的block中使用__block關鍵字修飾的對象時,block會retain該對象;而在MRC下卻不會retain。關於這點在官方文檔Transitioning to ARC Release Notes中有詳細的描述:
In manual reference counting mode, block id x; has the effect of not retaining x. In ARC mode, block id x; defaults to retaining x (just like all other values).
下面的代碼不管在MRC還是ARC中myController對象都是有內存洩露的:
MyViewController *myController = [[MyViewController alloc] init…]; // ... myController.completionHandler = ^(NSInteger result) { [myController dismissViewControllerAnimated:YES completion:nil]; };
內存洩露問題在MRC中可以按如下方式更改:
MyViewController * __block myController = [[MyViewController alloc] init…]; // ... myController.completionHandler = ^(NSInteger result) { [myController dismissViewControllerAnimated:YES completion:nil]; };
然而在ARC中這麼改就不行了。正如開始所說的那樣,在ARC中myController.completionHandler的block會retainmyController對象,使得內存洩露問題仍然存在!!
在ARC中該問題有兩種解決方案,第一種:
MyViewController * __block myController = [[MyViewController alloc] init…]; // ... myController.completionHandler = ^(NSInteger result) { [myController dismissViewControllerAnimated:YES completion:nil]; myController = nil; };
該方法在block中使用完myController時,是它指向nil。沒有strong類型的指針指向myController指向的對象時,對象會被釋放掉。
第二種種解決方案,直接使用weak代替block關鍵字:
MyViewController *myController = [[MyViewController alloc] init…]; // ... MyViewController * __weak weakMyViewController = myController; myController.completionHandler = ^(NSInteger result) { [weakMyViewController dismissViewControllerAnimated:YES completion:nil]; };
該方法直接避免了對block對myController對象的retain。
存在循環引用關系
如果self直接或者間接的對block存在強引用,在block中又需要使用self關鍵字,此時self和block就存在循環引用的關系。此時必須使用__weak關鍵字定義一個指針指向self,在block中使用該指針來引用self:
MessageListController * __weak weakSelf = self; self.messageLogic.loadMoreBlock = ^(IcarMessage * theMessage) { [weakSelf.tableView setPullTableIsLoadingMore:YES]; };
需要說明的是,盡管上例中weakSelf指針對self只是弱引用,但是self對block卻是強引用,self的生命周期一定是長於block的,因此不用擔心在block中使用weakSelf指針時,其指向的self會被釋放掉。
不存在循環引用關系
下面的例子:
MyViewController *myController = [[MyViewController alloc] init…]; // ... MyViewController * __weak weakMyController = myController; myController.completionHandler = ^(NSInteger result) { MyViewController *strongMyController = weakMyController; if (strongMyController) { // ... [strongMyController dismissViewControllerAnimated:YES completion:nil]; // ... } else { // Probably nothing... } };
如前面所說,myController.completionHandler的block中不能直接使用myController對象,會造成內存洩露,因此需要先用一個weak的指針指向myController對象,然後在block中使用該weak指針。但是為了確保在block執行的時候myController對象沒有被釋放掉,就在block一開始的地方定義了一個臨時的strong類型的指針strongMyController指向weak指針weakMyController,其實最終的結果就是block中對myController對象強引用了。在block執行完被銷毀的時候,strongMyController指針變量會被銷毀,其最終指向的myController對象因此也會被銷毀。這樣在使用一個對象的時候做就保證了該對象是存在的,使用完了再放棄該對象的所有權。
ARC和Toll-Free Bridging
MRC下的Toll-FreeBridging不涉及內存管理的轉移,Objective-C(後文簡稱OC)和Core Foundation(後文簡稱CF)各自管理各自的內存,相互之間可以直接交換使用,比如:
NSLocale *gbNSLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"]; CFLocaleRef gbCFLocale = (CFLocaleRef)gbNSLocale;
而在ARC下,事情就會變得復雜一些。因為ARC能夠管理OC對象的內存,卻不能管理CF對象,CF對象依然需要我們手動管理內存。在CF和OC之間bridge對象的時候,問題就出現了,編譯器不知道該如何處理這個同時有OC指針和CF指針指向的對象。這時候,需要使用__bridge, __bridge_retained, __bridge_transfer等修飾符來告訴編譯器該如何去做。
__bridge
它告訴編譯器仍然負責管理好在OC一端的引用計數的事情,開發者也繼續負責管理好在CF一端的事情,比如:
CFStringRef cfString = CFStringCreateWithCString(kCFAllocatorDefault, "CFString", kCFStringEncodingUTF8); NSString *ocString = (__bridge NSString *)cfString; CFRelease(cfString); NSLog(@"%@",ocString);
__bridge_retained 或 CFBridgingRetain
二者作用是一樣的,只是用法不同。
告訴編譯器需要retain對象,而開發者在CF一端負責釋放。這樣就算對象在OC一端被釋放,只要開發者不釋放CF一端的對象, 對象就不會被真的銷毀。
NSArray *ocArray = [[NSArray alloc] initWithObjects:@"foggry", nil]; CFArrayRef cfArray = (__bridge_retained CFArrayRef)ocArray; /** 使用cfArray **/ CFRelease(cfArray);
__bridge_transfer 或 CFBridgingRelease
二者作用也是一樣的,只是用法不同。
該關鍵字告訴編譯器bridge的同時,也轉移了對象的所有權,比如:
CFStringRef cfString = CFStringCreateWithCString(kCFAllocatorDefault, "CFString", kCFStringEncodingUTF8); NSString *ocString = (__bridge_transfer NSString *)cfString; //CFRelease(cfString); //不再需要釋放操作 NSLog(@"%@",ocString);
轉換過程中大家只需要根據具體需求選用適當的關鍵字即可。
另外,在ARC中id和void *也不能直接相互轉換了,必須通過Toll-FreeBridging使用適當的關鍵字修飾。
ARC和IBOutLet
對於IBOutLet屬性應該用strong還是weak一直都有疑惑。關於這一點官方文檔是這麼介紹的:
From a practical perspective, in iOS and OS X outlets should be defined as declared properties. Outlets should generally be weak, except for those from File’s Owner to top-level objects in a nib >>>file (or, in iOS, a storyboard scene) which should be strong. Outlets that you create should therefore typically be weak.
那麼長的一段英文想說的是:如果nib文件構建的view是直接被Controller引用的頂層view,對應的IBOutLet屬性應該是strong;
如果view是頂層view上的一個子view,那麼該view的屬性應該是weak,因為頂層view被Controller使用strong屬性引用了,而頂層view本身又持有該view;
如果Controller對某個view需要單獨引用,或者Controller沒有引用某個view的父view,那麼其屬性也應該是strong。
好吧,其實我能說如果你實在懶得區分什麼時候用strong,什麼時候用weak,那就將所以後的IBOutLet屬性都設成strong吧!在Controller銷毀的時候,對應的IBOutLet實例變量也會被銷毀,strong指針會被置成nil,因此也不會有內存問題。
參考文檔
Transitioning to ARC Release Notes
Managing the Lifetimes of Objects from Nib Files
Nib Memory Management