轉向ARC實際操練

 這是緊接上一篇ARC介紹的文字,原創作者通過一個實例,講述瞭如何從現有的非ARC項目轉向ARC項目,值得大家學習。

 

具體操作

說了這麼多,終於可以實踐一下了。在決定使用ARC後,很多開發者面臨的首要問題是不知如何下手。因爲可能手上的項目已經用MRC寫了一部分,不想麻煩做轉變;或者因爲新項目裏用ARC時遇到了奇怪的問題,從而放棄ARC退回MRC。這都是常見的問題,而在下面,將通過一個demo引導大家徹底轉向ARC的世界。

例子很簡單,這是一個查找歌手的應用,包含一個簡單的UITableView和一個搜索框,當用戶在搜索框搜索時,調用MusicBrainz的API完成名字搜索和匹配。MusicBrainz是一個開放的音樂信息平臺,它提供了一個免費的XML網頁服務,如果對MusicBrainz比較有興趣的話,可以到它的官網逛一逛。

Demo的起始例子可以從這裏下載,爲了照顧新人,在這邊進行簡單說明。在Xcode中打開下載的例子,應該可以看到如下內容(Xcode和iOS開發熟練者請跳過此段)

AppDelegate.h/m 這是整個app的delegate,沒什麼特殊的,每個iOS/Mac程序在main函數以後的入口,由此進入app的生命週期。在這裏加載了最初的viewController並將其放到Window中展示出來。另外appDelegate還負責處理程序開始退出等系統委託的事件

MainViewController.h/m/xib 這個demo最主要的ViewController,含有一個TableView和一個搜索條。 SoundEffect.h/m 簡單的播放聲音的類,在MusicBrainz搜索完畢時播放一個音效。 main.m 程序入口,所有c程序都從main函數開始執行

AFHTTPRequestOperation.h/m 這是有名的網絡框架AFNetworking的一部分,用來幫助等簡單地處理web服務請求。這裏只包含了這一個類而沒有將全部的AFNetworking包括進來,因爲我們只用了這一個類。完整的框架代碼可以在github的相關頁面上找到https://github.com/gowalla/AFNetworking

SVProgresHUD.h/m/bundle 是一個常用的進度條指示,當搜索的時候出現以提示用戶正在搜索請稍後。bundle是資源包,裏面包含了幾張該類用到的圖片,打進bundle包的目的一方面是爲了資源容易管理,另一方面也是主要方面時爲了不和其他資源發生衝突(Xcode中資源名字是資源的唯一標識,同名字的資源只能出現一次,而放到bundle包裏可以避免這個潛在的問題)。SVProgresHUD可以在這裏找到https://github.com/samvermette/SVProgressHUD

快速過一遍這個應用吧:MainViewController是UIViewController的子類,對應的xib文件定義了對應的UITableView和UISearchBar。TableView中顯示searchResult數組中的內容。當用戶搜索時,用AFHTTPRequestOperation發一個HTTP請求,當從MusicBrainz得到迴應後將結果放入searchResult數組中並用tableView顯示,當返回結果是空時在tableView中顯示沒找到。主要的邏輯都在MainViewController.m中的-searchBarSearchButtonClicked:方法中,生成了用於查詢的URL,根據MusicBrainz的需求替換了請求的header,並且完成了返回邏輯,然後在主線程中刷新UI。整個程序還是比較簡單的~

MRC到ARC的自動轉換

回到正題,我們討論的是ARC,關於REST API和XML解析的技術細節就暫時先忽略吧..整個程序都是用MRC來進行內存管理的,首先來讓我們把這個demo轉成ARC吧。基本上轉換爲ARC意味着把所有的retain,release和autorelease關鍵字去掉,在之前我們明確幾件事情:
* Xcode提供了一個ARC自動轉換工具,可以幫助你將源碼轉爲ARC
* 當然你也可以自己動手完成ARC轉換
* 同時你也可以指定對於某些你不想轉換的代碼禁用ARC,這對於很多龐大複雜的還沒有轉至ARC的第三方庫幫助很大,因爲不是你寫的代碼你想動手修改的話代碼超級容易mess…

對於我們的demo,爲了說明問題,這三種策略我們都將採用,注意這僅僅只是爲了展示如何轉換。實際操作中不需要這麼麻煩,而且今後的絕大部分情況應該是從工程建立開始就是ARC的。

首先,ARC是LLVM3.0編譯器的特性,而老的工程特別是Xcode3時代的工程的默認編譯器很可能是GCC或者LLVM-GCC,因此第一步就是確認編譯器是否正確。在Project設置面板,選擇target,在Build Settings中將Compiler for C/C++/Objective-C選爲Apple LLVMcompiler 3.0或以上。爲了確保之後轉換的順利,在這裏我個人建議最好把Treat Warnings as Errors和 Run Static Analyzer都打開,確保在改變編譯器後代碼依舊沒有警告或者內存問題(雖然靜態分析可能不太能保證這一點,但是聊勝於無)。好了~clean(Shift+Cmd+K)以後Bulid一下試試看,經過修改後的demo工程沒有任何警告和錯誤,這是很好的開始。(對於存在警告的代碼,這裏是很好的修復的時機..請在轉換前確保原來的代碼沒有內存問題)。

接下來就是完成從MRC到ARC的偉大轉換了。還是在Build Settings頁面,把Objective-C Automatic Reference Counting改成YES(如果找不到的話請看一看搜索欄前面的小標籤是不是調成All了..這個選項在Basic裏是不出現的),這樣我們的工程就將在所有源代碼中啓用ARC了。然後…試着編譯一下看看,嗯..無數的錯誤。

這是很正常的,因爲ARC裏不允許出現retain,release之類的,而MRC的代碼這些是肯定會有的東西。我們可以手動一個一個對應地去修復這些錯誤,但是這很麻煩。Xcode爲我們提供了一個自動轉換工具,可以幫助重寫源代碼,簡單來說就是去掉多餘的語句並且重寫一些property關鍵字。

這個小工具是Edit->Refactor下的Convert to Objective-C ARC,點擊後會讓我們選擇要轉換哪幾個文件,在這裏爲了說明除了自動轉換外的方法,我們不全部轉換,而只是選取其中幾個轉換(MainViewController.m和AFHTTPRequestOperation.m不做轉換)。注意到這個對話框上有個警告標誌告訴我們target已經是ARC了,這是由於之前我們在Build Settings裏已經設置了啓用ARC,其實直接在這裏做轉換後Xcode會自動幫我們開啓ARC。點擊檢查後,Xcode告訴我們一個不幸的消息,不能轉換,需要修復ARC readiness issues..後面還告訴我們要看到所有的所謂的ARC readiness issues,可以到設置的General裏把Continue building after errors勾上…What the f**k…好吧~先乖乖聽從Xcode的建議”Cmd+,“然後Continue building after errors打勾然後再build。

問題依舊,不過在issue面板裏應該可以看到所有出問題的代碼了。在我們的例子裏,問題出在SoundEffect.m裏:

  1. NSURL *fileURL = [[NSBundle mainBundle] URLForResource:filename withExtension:nil]; 
  2.  
  3. if (fileURL != nil) 
  4.  
  5.  
  6.     SystemSoundID theSoundID; 
  7.  
  8.     OSStatus error = AudioServicesCreateSystemSoundID((CFURLRef)fileURL, &theSoundID); 
  9.  
  10.     if (error == kAudioServicesNoError) 
  11.  
  12.         soundID = theSoundID; 
  13.  

這裏代碼嘗試把一個NSURL指針強制轉換爲一個CFURLRef指針。這裏涉及到一些Core Services特別是Core Foundation(CF)的東西,AudioServicesCreateSystemSoundID()函數接受CFURLRef爲參數,這是一個CF的概念,但是我們在較高的抽象層級上所建立的是NSURL對象。在Cocoa框架中,有很多頂層對象對底層的抽象,而在使用中我們往往可以不加區別地對這兩種對象進行同樣的對待,這類對象即爲可以”自由橋接”的對象(toll-free bridged)。NSURL和CFURLRef就是一對好基友好例子,在這裏其實CFURLRef和NSURL是可以進行替換的。

通常來說爲了代碼在底層級上的正確,在iOS開發中對基於C的API的調用所傳入的參數一般都是CF對象,而Objective-C的API調用都是傳入NSObject對象。因此在採用自由橋接來調用C API的時候就需要進行轉換。但是在使用ARC編譯的時候,因爲內存管理的原因,編譯器需要知道對這些橋接對象要實行什麼樣的操作。如果一個NSURL對象替代了CFURLRef,那麼在作用區域外,應該由誰來決定內存釋放和對象銷燬呢?爲了解決這個問題,引入了bridge,bridge_transfer和__bridge_retained三個關鍵字。關於選取哪個關鍵字做轉換,需要由實際的代碼行爲來決定。如果對於自由橋接機制感興趣,大家可以自己找找的相關內容,比如適用類型內部機制一個簡介~之後我也會對這個問題做進一步說明

回到demo,我們現在在上面的代碼中加上__bridge進行轉換。然後再運行ARC轉換工具,這時候檢查應該沒有其他問題了,那麼讓我們進行轉換吧~當然在真正轉換之前會有一個預覽界面,在這裏我們最好檢查一下轉換是不是都按照預想進行了..要是出現大面積錯誤又沒有備份或者出現各種意外的話就可以哭了…

前後變化的話比較簡單,基本就是去掉不需要的代碼和改變property的類型而已,其實有信心的話不太需要每次都看,但是如果是第一次執行ARC轉換的操作的話,我還是建議稍微看一下變化,這樣能對ARC有個直觀上的瞭解。檢查一遍,應該沒什麼問題了..需要注意的是main.m裏關於autoreleasepool的變化以及所有dealloc調用裏的[super dealloc]的刪除,它們同樣是MRC到ARC的主要變化..

好了~轉換完成以後我們再build看看..應該會有一些警告。對於原來retain的property,比較保險的做法是轉爲strong,在LLVM3.0中自動轉換是這樣做的,但是在3.1中property默認並不是strong,這樣在使用property賦值時存在警告,我們在property聲明裏加上strong就好了~然後就是SVProgressHUD.m裏可能存在問題,這是由於原作者把release的代碼和其他代碼寫在一行了.導致自動轉換時只刪掉了部分,而留下了部分不應該存在的代碼,刪掉對變量的空調用就好了..

自動轉換之後的故事

然後再編譯,沒有任何錯誤和警告了,好棒~等等…我們剛纔沒有對MainViewController和AFHTTPRequestOperation進行處理吧,那麼這兩個文件裏應該還存在release之類的東西吧..?看一看這兩個文件,果然有各種release,但是爲什麼能編譯通過呢?!明明剛纔在自動轉換前他們還有N多錯的嘛…答案很簡單,在自動轉換的時候因爲我們沒有勾選這兩個文件,因此編譯器在自動轉換過後爲這兩個文件標記了”不使用ARC編譯”。可以看到在target的Building Phases下,MainViewController.m和AFHTTPRequestOperation.m兩個文件後面被加上了-fno-objc-arc的編譯標記,被加上該標記的文件將不使用ARC規則進行編譯。

提供這樣的編譯標記的原因是顯而易見的,因爲總是有一部分的第三方代碼並沒有轉換爲ARC(可能是由於維護者犯懶或者已經終止維護),所以對於這部分代碼,爲了迅速完成轉換,最好是使用-fno-objc-arc標記來禁止在這些源碼上使用ARC。

爲了方便查找,再此列出一些在轉換時可能出現的問題,當然在我們使用ARC時也需要注意避免代碼中出現這些問題:

  • “Cast … requires a bridged cast”

這是我們在demo中遇到的問題,不再贅述

  • Receiver type ‘X’ for instance message is a forward declaration

這往往是引用的問題。ARC要求完整的前向引用,也就是說在MRC時代可能只需要在.h中申明@class就可以,但是在ARC中如果調用某個子類中未覆蓋的父類中的方法的話,必須對父類.h引用,否則無法編譯。

  • Switch case is in protected scope

現在switch語句必須加上{}了,ARC需要知道局部變量的作用域,加上{}後switch語法更加嚴格,否則遇到沒有break的分支的話內存管理會出現問題。

  • A name is referenced outside the NSAutoreleasePool scope that it was declared in

這是由於寫了自己的autoreleasepool,而在轉換時在原來的pool中申明的變量在新的@autoreleasepool中作用域將被侷限。解決方法是把變量申明拿到pool的申請之前。

  • ARC forbids Objective-C objects in structs or unions

可以說ARC所引入的最嚴格的限制是不能在C結構體中放OC對象了..因此類似下面這樣的代碼是不可用的

  1. typedef struct {  
  2.  
  3.     UIImage *selectedImage;  
  4.  
  5.     UIImage *disabledImage;  
  6.  
  7. } ButtonImages; 

這個問題只有乖乖想辦法了..改變原來的結構什麼的..

 

手動轉換

剛纔做了對demo的大部分轉換,還剩下了MainViewController和AFHTTPRequestOperation是MRC。但是由於使用了-fno-objc-arc,因此現在編譯和運行都沒有問題了。下面我們看看如何手動把MainViewController轉爲ARC,這也有助於進一步理解ARC的規則。

首先,我們需要轉變一下觀念…對於MainViewController.h,在.h中申明瞭兩個實例變量:

  1. @interface MainViewController : UIViewController   
  2.  
  3. {  
  4.  
  5.     NSOperationQueue *queue; 
  6.  
  7.     NSMutableString *currentStringValue;  
  8.  

我們不妨仔細考慮一下,爲什麼在interface裏出現了實例變量的申明?通常來說,實例變量只是在類的實例中被使用,而你所寫的類的使用者並沒有太多必要了解你的類中有哪些實例變量。而對於絕大部分的實例變量,應該都是protected或者private的,對它們的操作只應該用setter和getter,而這正是property所要做的工作。可以說,將實例變量寫在頭文件中是一種遺留的陋習。更好的寫實例變量名字的地方應當與類實現關係更爲密切,爲了隱藏細節,我們應該考慮將它們寫在@implementation裏。好消息是,在LLVM3.0中,不論是否開啓ARC,編譯器是支持將實例變量寫到實現文件中的。甚至如果沒有特殊需要又用了property,我們都不應該寫無意義的實例變量申明,因爲在@synthesize中進行綁定時,我們就可以設置變量名字了,這樣寫的話可以讓代碼更加簡潔。

在這裏我們對着兩個實例變量不需要property(外部成員不應當能訪問到它們),因此我們把申明移到.m裏中。修改後的.h是這樣的,十分簡潔一看就懂~

  1. #import  
  2.  
  3. @interface MainViewController : UIViewController 
  4.  
  5. @property (nonatomic, retain) IBOutlet UITableView *tableView;   
  6.  
  7. @property (nonatomic, retain) IBOutlet UISearchBar *searchBar;  
  8.  
  9. @end 

然後.m的開頭變成這樣:

  1. @implementation MainViewController  
  2.  
  3. {  
  4.  
  5.     NSOperationQueue *queue;   
  6.  
  7.     NSMutableString *currentStringValue;   
  8.  

這樣的寫法讓代碼相當靈活,而且不得不承認.m確實是這些實例變量的應該在的地方…build一下,沒問題..當然對於SoundEffect類也可以做相似的操作,這會讓使用你的類的人很開心,因爲.h越簡單越好..P.S.另外一個好處可以減少.h裏的引用,減少編譯時間(雖然不明顯=。=)

然後就可以在MainViewController裏啓用ARC了,方法很簡單,刪掉Build Phases裏相關文件的-fno-objc-arc標記就可以了~然後..然後當然是一大堆錯誤啦。我們來手動一個個改吧,雖然談不上樂趣,但是成功以後也會很有成就~(如果你不幸在啓用ARC後build還是成功了,恭喜你遇到了Xcode的bug,請Cmd+Q然後重新打開Xcode把=_=)

dealloc

紅色最密集的地方是dealloc,因爲每一行都是release。由於在這裏dealloc並沒有做除了release和super dealloc之外的任何事情,因此簡單地把整個方法刪掉就好了。當然,在對象被銷燬時,dealloc還是會被調用的,因此我們在需要對非ARC管理的內存進行管理和必要的邏輯操作的時候,還是應該保留dealloc的,當然這涉及到CF以及以下層的東西:比如對於retain的CF對象要CFRelease(),對於malloc()到堆上的東西要free()掉,對於添加的observer可以在這裏remove,schedule的timer在這裏invalidate等等~[super dealloc]這個消息也不再需要發了,ARC會自動幫你搞定。

另外,在MRC時代一個常做的事情是在dealloc裏把指向自己的delegate設成nil(否則就等着EXC_BAD_ACCESS吧 :P ),而現在一般delegate都是weak的,因此在self被銷燬後這個指針自動被置成nil了,你不用再爲之擔心,好棒啊..

去掉各種release和autorelease

這個很直接,沒有任何問題。去掉就行了~不再多說

討論一下Property

在MainViewController.m裏的類擴展中定義了兩個property:

  1. @interface MainViewController () 
  2.         
  3. @property (nonatomic, retain) NSMutableArray *searchResults; 
  4.        
  5. @property (nonatomic, retain) SoundEffect *soundEffect;  
  6.     
  7. @end 

申明的類型是retain,關於retain,assign和copy的討論已經爛大街了,在此不再討論。在MRC的年代使用property可以幫助我們使用dot notation的時候簡化對象的retain和copy,而在ARC時代,這就顯得比較多餘了。在我看來,使用property和點方法來調用setter和getter是不必要的。property只在將需要的數據在.h中暴露給其他類時才需要,而在本類中,只需要用實例變量就可以。因此我們可以移去searchResults和soundEffect的@property和@synthesize,並將起移到實例變量申明中:

  1. @implementation MainViewController 
  2.  
  3. {  
  4.  
  5.     NSOperationQueue *queue;  
  6.  
  7.     NSMutableString *currentStringValue; 
  8.  
  9.     NSMutableArray *searchResults; 
  10.  
  11.     SoundEffect *soundEffect;  
  12.  

相應地,我們需要將對應的self.searchResult和self.soundEffect的self.都去去掉。在這裏需要注意的是,雖然我們去掉了soundEffect的property和synthesize,但是我們依然有一個lazy loading的方法- (SoundEffect *)soundEffect,神奇之處在於(可能你以前也不知道),點方法並不需要@property關鍵字的支持,雖然大部分時間是這麼用的..(property只是對setter或者getter的申明,而點方法是對其的調用,在這個例子的實現中我們事實上實現了-soundEffect這個getter方法,所以點方法在等號右邊的getter調用是沒有問題的)。爲了避免誤解,建議把self.soundEffect的getter調用改寫成[self soundEffect]。

然後我們看看.h裏的property~裏面有兩個retain的IBOutlet。retain關鍵字在ARC中是依舊可用的,它在ARC中所扮演的角色和strong完全一樣。爲了避免迷惑,最好在需要的時候將其寫爲strong,那樣更符合ARC的規則。對於這兩個property,我們將其申明爲weak(事實上,如果沒有特別意外,除了最頂層的IBOutlet意外,自己寫的outlet都應該是weak)。通過加載xib得到的用戶界面,在其從xib文件加載時,就已經是view hierarchy的一部分了,而view hierarchy中的指向都是strong的。因此outlet所指向的UI對象不應當再被hold一次了。將這些outlet寫爲weak的最顯而易見的好處是你就不用再viewDidUnload方法中再將這些outlet設爲nil了(否則就算view被摧毀了,但是由於這些UI對象還在被outlet指針指向而無法釋放,代碼簡潔了很多啊..)。

在我們的demo中將IBOutlet的property改爲weak並且刪掉viewDidUnload中關於這兩個IBOutlet的內容~

總結一下新加入的property的關鍵字類型:

  • strong 和原來的retain比較相似,strong的property將對應__strong的指針,它將持有所指向的對象

  • weak 不持有所指向的對象,而且當所指對象銷燬時能將自己置爲nil,基本所有的outlet都應該用weak

  • unsafe_unretained 這就是原來的assign。當需要支持iOS4時需要用到這個關鍵字

  • copy 和原來基本一樣..copy一個對象並且爲其創建一個strong指針

  • assign 對於對象來說應該永遠不用assign了,實在需要的話應該用unsafe_unretained代替(基本找不到這種時候,大部分assign應該都被weak替代)。但是對於基本類型比如int,float,BOOL這樣的東西,還是要用assign。

特別地,對於NSString對象,在MRC時代很多人喜歡用copy,而ARC時代一般喜歡用strong…(我也不懂爲什麼..求指教)

自由橋接的細節

MainViewController現在剩下的問題都是橋接轉換問題了~有關橋接的部分有三處:

  • (NSString *)CFURLCreateStringByAddingPercentEscapes(…):CFStringRef至NSString *

  • (CFStringRef)text:NSString *至CFStringRef

  • (CFStringRef)@“!‘();:@&=+$,/?%#[]“:NSString 至CFStringRef

編譯器對前兩個進行了報錯,最後一個是常量轉換不涉及內存管理。

關於toll-free bridged,如果不進行細究,NSString和CFStringRef是一樣的東西,新建一個CFStringRef可以這麼做:

  1. CFStringRef s1 = [[NSString alloc] initWithFormat:@"Hello, %@!",name]; 

然後,這裏alloc了而s1是一個CF指針,要釋放的話,需要這樣:

  1. CFRelease(s1); 

相似地可以用CFStringRef來轉成一個NSString對象(MRC):

  1. CFStringRef s2 = CFStringCreateWithCString(kCFAllocatorDefault,bytes, kCFStringEncodingMacRoman);  
  2.  
  3. NSString *s3 = (NSString *)s2;  
  4.  
  5. // release the object when you're done  
  6.  
  7. [s3 release]; 

在ARC中,編譯器需要知道這些指針應該由誰來負責釋放,如果把一個NSObject看做是CF對象的話,那麼ARC就不再負責它的釋放工作(記住ARC是only for NSObject的)。對於不需要改變持有者的對象,直接用簡單的bridge就可以了,比如之前在SoundEffect.m做的轉換。在這裏對於(CFStringRef)text這個轉換,ARC已經負責了text這個NSObject的內存管理,因此這裏我們需要一個簡單的bridge。而對於CFURLCreateStringByAddingPercentEscapes方法,方法中的create暗示了這個方法將形成一個新的對象,如果我們不需要NSString轉換,那麼爲了避免內存的問題,我們需要使用CFRelease來釋放它。而這裏我們需要一個NSString,因此我們需要告訴編譯器接手它的內存管理工作。這裏我們使用bridge_transfer關鍵字,將內存管理權由CF object移交給NSObject(或者說ARC)。如果這裏我們只用bridge的話,內存管理的負責人沒有改變,那麼這裏就會出現一個內存泄露。另外有時候會看到CFBridgingRelease(),這其實就是transfer cast的內聯寫法..是一樣的東西。總之,需要記住的原則是,當在涉及CF層的東西時,如果函數名中有含有Create, Copy, 或者Retain之一,就表示返回的對象的retainCount+1了,對於這樣的對象,最安全的做法是將其放在CFBridgingRelease()裏,來平衡retain和release。

還有一種bridge方式,__bridge_retained。顧名思義,這種轉換將在轉換時將retainCount加1。和CFBridgingRelease()相似,也有一個內聯方法CFBridgingRetain()來負責和CFRelease()進行平衡。

需要注意的是,並非所有的CF對象都是自由橋接的,比如Core Graphics中的所有對象都不是自由橋接的(如CGImage和UIImage,CGColor和UIColor)。另外也不是隻有自由橋接對象才能用bridge來橋接,一個很好的特例是void (指向任意對象的指針,類似id),對於void 和任意對象的轉換,一般使用_bridge。(這在將ARC運用在Cocos2D中很有用)

終於搞定了

至此整個工程都ARC了~對於AFHTTPRequestOperation這樣的不支持ARC的第三方代碼,我們的選擇一般都是就不使用ARC了(或者等開源社區的大大們更新ARC適配版本)。可以預見,在近期會有越來越多的代碼轉向ARC,但是也一定會有大量的代碼暫時或者永遠保持MRC等個,所以對於這些代碼就不用太糾結了~


寫在最後

寫了那麼多,希望你現在能對ARC有個比較全面的瞭解和認識了。ARC肯定是以後的趨勢,也確實能讓代碼量大大降低,減少了很多無意義的重複工作,還提高了app的穩定性。但是凡事還是紙上得來終覺淺,希望作爲開發者的你,在下一個工程中去嘗試用用ARC~相信你會和我一樣,馬上愛上這種make life easier的方式的~

 

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