ARC是什么
ARC是ios5推出的新功能,全稱叫ARC(Automatic Reference Counting)。簡單地說,就是代碼中自動加入了retain/release,原先需要手動添加的用來處理內存管理的引用計數的代碼可以自動地由編譯器完成了。
簡單理解ARC,就是通過指定的語法,讓編譯器(LLVM3.0)在編譯代碼時,自動生成實例的引用計數管理部分代碼。有一點,ARC并不是GC,它只是一種代碼靜態分析(static Analyzer)工具。
ARC的本質
ARC是編譯器特性,而不是運行時特性,更不是垃圾回收器(CG)。
ARC的開啟與關閉
打開ARC:-fobjc-arc
關閉ARC:-fno-objc-arc
ARC的修飾符
__strong:表示引用為強引用。對應的定義property時的”strong”。所有對象只有當沒有任何一個強引用指向時,才會被釋放。
注意:如果在聲明引用時不加修飾符,那么引用將默認是強引用。當需要釋放強引用指向的對象時,需要將強引用置nil。
__weak:表示引用為弱引用。對應在定義property時的”weak”。弱引用不會影響對象的釋放,即只要對象沒有任何一個強引用指向,即使有100個弱引用對象指向也沒用,該對象會被釋放。不過好在,對象在被釋放的同時,指向它的弱引用會自動被置nil,這個技術叫zeroing weak pointer。這樣有效得防止無效指針,野指針的產生。__weak一般用在delegate關系中防止循環引用或者用來修飾指向由Interface Builder編輯與生成的UI控件。
__autoreleasing:表示在autorelease pool中自動釋放對象的引用,和MRC時代autorelease的用法相同。定義property時不能使用這個修飾符,任何一個對象的property都不應該是autorelease型的。
以下兩行代碼的意義是相同的。
為了提高效率,避免這種情況,我們一般在定義error的時候將其聲明為__autorelease類型的
*error指向的對象在創建出來后,被放入到了autorelease pool中,等待使用結束后的自動釋放,函數外error的使用者并不需要關心*error指向對象的釋放。
另外一點,在ARC中,所有這種指針的指針(NSError**)的函數參數如果不加修飾符,編譯器會默認將他們認定為__autoreleasing類型。
比如下面的兩端代碼是等同的
除非你顯式得給value聲明了__strong,否則value默認就是__autoreleasing的。
最后一點,某些類的方法會隱式地使用自己的autorelease pool,在這種時候使用__autoreleasing類型要特別小心。
比如NSDictionary的[enumerateKeysAndObjectsUsingBlock]方法:
會隱式地創建一個autorelease pool,上面代碼實際類似于:
為了能夠正常的使用*error,我們需要一個strong型的臨時引用,在dict的枚舉Block中是用這個臨時引用,保證引用指向的對象不會在出了dict的枚舉Block后被釋放,正確的方式如下:
__unsafe_unretained:ARC是在iOS 5引入的,而這個修飾符主要是為了在ARC剛發布時兼容iOS 4以及版本更低的設備,因為這些版本的設備沒有weak pointer system,簡單的理解這個系統就是我們上面講weak時提到的,能夠在weak引用指向對象被釋放后,把引用值自動設為nil的系統。這個修飾符在定義property時對應的是”unsafe_unretained”,實際可以將它理解為MRC時代的assign:純粹只是將引用指向對象,沒有任何額外的操作,在指向對象被釋放時依然原原本本地指向原來被釋放的對象(所在的內存區域)。所以非常不安全。
使用修飾符的正確姿勢
要定義一個weak型的NSString引用,它的寫法應該是:
棧中指針默認值為nil
無論是被strong,weak還是autoreleasing修飾,聲明在棧中的指針默認值都會是nil。所有這類型的指針不用再初始化的時候置nil了。雖然好習慣是最重要的,但是這個特性更加降低了“野指針”出現的可能性。
在ARC中,以下代碼會輸出null而不是crash:)
ARC與Block
在MRC時代,Block會隱式地對進入其作用域內的對象(或者說被Block捕獲的指針指向的對象)加retain,來確保Block使用到該對象時,能夠正確的訪問。
這件事情在下面代碼展示的情況中要更加額外小心。
在這段代碼中,myController的completionHandler調用了myController的方法[dismissViewController…],這時completionHandler會對myController做retain操作。而我們知道,myController對completionHandler也至少有一個retain(一般準確講是copy),這時就出現了在內存管理中最糟糕的情況:循環引用!簡單點說就是:myController retain了completionHandler,而completionHandler也retain了myController。循環引用導致了myController和completionHandler最終都不能被釋放。我們在delegate關系中,對delegate指針用weak就是為了避免這種問題。
不過好在,編譯器會及時地給我們一個警告,提醒我們可能會發生這類型的問題:
對這種情況,我們一般用如下方法解決:給要進入Block的指針加一個__block修飾符。
這個__block在MRC時代有兩個作用:
1.說明變量可改
2.說明指針指向的對象不做這個隱式的retain操作
一個變量如果不加__block,是不能在Block里面修改的,不過這里有一個例外:static的變量和全局變量不需要加__block就可以在Block中修改。
使用這種方法,我們對代碼做出修改,解決了循環引用的問題:
在ARC引入后,沒有了retain和release等操作,情況也發生了改變:在任何情況下,__block修飾符的作用只有上面的第一條:說明變量可改。即使加上了__block修飾符,一個被block捕獲的強引用也依然是一個強引用。這樣在ARC下,如果我們還按照MRC下的寫法,completionHandler對myController有一個強引用,而myController對completionHandler有一個強引用,這依然是循環引用,沒有解決問題:(
于是我們還需要對原代碼做修改。簡單的情況我們可以這樣寫:
在completionHandler之后將myController指針置nil,保證了completionHandler對myController強引用的解除,不過也同時解除了myController對myController對象的強引用。這種方法過于簡單粗暴了,在大多數情況下,我們有更好的方法。
這個更好的方法就是使用weak。(或者為了考慮iOS4的兼容性用unsafe_unretained,具體用法和weak相同,考慮到現在iOS4設備可能已經絕跡了,這里就不講這個方法了)(關于這個方法的本質我們后面會談到)
為了保證completionHandler這個Block對myController沒有強引用,我們可以定義一個臨時的弱引用weakMyViewController來指向原myController的對象,并把這個弱引用傳入到Block內,這樣就保證了Block對myController持有的是一個弱引用,而不是一個強引用。如此,我們繼續修改代碼:
這樣循環引用的問題就解決了,但是卻不幸地引入了一個新的問題:由于傳入completionHandler的是一個弱引用,那么當myController指向的對象在completionHandler被調用前釋放,那么completionHandler就不能正常的運作了。在一般的單線程環境中,這種問題出現的可能性不大,但是到了多線程環境,就很不好說了,所以我們需要繼續完善這個方法。
為了保證在Block內能夠訪問到正確的myController,我們在block內新定義一個強引用strongMyController來指向weakMyController指向的對象,這樣多了一個強引用,就能保證這個myController對象不會在completionHandler被調用前釋放掉了。于是,我們對代碼再次做出修改:
到此,一個完善的解決方案就完成了:)
理解這個問題的關鍵在于理解被Block捕獲的引用和在Block內定義的引用的區別。
為了更清楚地說明問題,這里用一個簡單的程序舉例。比如我們有如下程序:
程序中,同為int型的指針,a是被Block捕獲的變量,而c是在Block內定義的變量。我們用clang -rewrite-objc處理后,可以看到如下代碼:
Block的結構:
實際執行的函數:
我們可以清楚得看到,a和c存在的位置完全不同,如果Block存在于堆上(在ARC下Block默認在堆上),那么a作為Block結構體的一個成員,也自然會存在于堆上,而c無論如何,永遠位于Block內實際執行代碼的函數棧內。這也導致了兩個變量生命周期的完全不同:c在Block的函數運行完畢,即會被釋放,而a呢,只有在Block被從堆上釋放的時候才會釋放。
回到我們的MyViewController的例子中,同上理,如果我們直接讓Block捕獲我們的myController引用,那么這個引用會被復制后(引用類型也會被復制)作為Block的成員變量存在于其所在的堆空間中,也就是為Block增加了一個指向myController對象的強引用,這就是造成循環引用的本質原因。對于MyViewController的例子,Block的結構體可以理解是這個樣子:(準確的結構體肯定和以下這個有區別,但也肯定是如下這種形式:)
而反觀我們給Block傳入一個弱引用weakMyController,這時我們Block的結構:
再看在Block內聲明的強引用strongMyController,它雖然是強引用,但存在于函數棧中,在函數執行期間,它一直存在,所以myController對象也一直存在,但是當函數執行完畢,strongMyController即被銷毀,于是它對myController對象的強引用也被解除,這時Block對myController對象就不存在強引用關系了!加入了strongMyController的函數大體會是這個樣子:
綜上所述,在ARC下(在MRC下會略有不同),Block捕獲的引用和Block內聲明的引用無論是存在空間與生命周期都是截然不同的,也正是這種不同,造成了我們對他們使用方式的區別。
以上就解釋了之前提到的所有問題,希望大家能看明白:
好的,最后再提一點,在ARC中,對Block捕獲對象的內存管理已經簡化了很多,由于沒有了retain和release等操作,實際只需要考慮循環引用的問題就行了。比如下面這種,是沒有內存泄露的問題的:
我們上面提到的解決方案,只是針對Block產生循環引用的問題,而不是說所有的Block捕獲引用都要這么處理,一定要注意!
ARC與Toll-Free Bridging
在MRC時代,由于Objective-C類型的對象和Core Foundation類型的對象都是相同的release和retain操作規則,所以Toll-Free Bridging的使用比較簡單,但是自從ARC加入后,Objective-C類型的對象內存管理規則改變了,而Core Foundation依然是之前的機制,換句話說,Core Foundation不支持ARC。
這個時候就必須要要考慮一個問題了,在做Core Foundation與Objective-C類型轉換的時候,用哪一種規則來管理對象的內存。顯然,對于同一個對象,我們不能夠同時用兩種規則來管理,所以這里就必須要確定一件事情:哪些對象用Objective-C(也就是ARC)的規則,哪些對象用Core Foundation的規則(也就是MRC)的規則。或者說要確定對象類型轉換了之后,內存管理的ownership的改變。
__bridge(修飾符)
只是聲明類型轉變,但是不做內存管理規則的轉變。
只是做了NSString到CFStringRef的轉化,但管理規則未變,依然要用Objective-C類型的ARC來管理s1,你不能用CFRelease()去釋放s1。
__bridge_retained(修飾符) or CFBridgingRetain(函數)
表示將指針類型轉變的同時,將內存管理的責任由原來的Objective-C交給Core Foundation來處理,也就是,將ARC轉變為MRC。
比如,還是上面那個例子
我們在第二行做了轉化,這時內存管理規則由ARC變為了MRC,我們需要手動的來管理s2的內存,而對于s1,我們即使將其置為nil,也不能釋放內存。
等同的,我們的程序也可以寫成:
__bridge_transfer(修飾符) or CFBridgingRelease(函數)
這個修飾符和函數的功能和上面那個__bridge_retained相反,它表示將管理的責任由Core Foundation轉交給Objective-C,即將管理方式由MRC轉變為ARC。
比如:
這里我們將result的管理責任交給了ARC來處理,我們就不需要再顯式地將CFRelease()了。
對了,這里你可能會注意到一個細節,和ARC中那個4個主要的修飾符(__strong,__weak,…)不同,這里修飾符的位置是放在類型前面的,雖然官方文檔中沒有說明,但看官方的頭文件可以知道。小伙伴們,記得別把位置寫錯哦:)
下圖為今年部分iOS開發的視頻教程,因為不定時更新中故不做多的截圖,如果有iOS開發上的問題不懂或者需要視頻教程可以看我的個人簡介。
不定時更新中。