引言:
ARC的出生及成長背景
蘋果在 2011 年的時候,在 WWDC 大會上提出了自動的引用計數(shù)(ARC)。ARC 背后的原理是依賴編譯器的靜態(tài)分析能力,通過在編譯時找出合理的插入引用計數(shù)管理代碼,從而徹底解放程序員。
在 ARC 剛剛出來的時候,業(yè)界對此黑科技充滿了懷疑和觀望,加上現(xiàn)有的 MRC 代碼要做遷移本來也需要額外的成本,所以 ARC 并沒有被很快接受。直到 2013 年左右,蘋果認為 ARC 技術(shù)足夠成熟,直接將 macOS(當時叫 OS X)上的垃圾回收機制廢棄,從而使得 ARC 迅速被接受。
2014 年的 WWDC 大會上,蘋果推出了 Swift 語言,而該語言仍然使用 ARC 技術(shù),作為其內(nèi)存管理方式。
以下是引用唐巧大神的話:
為什么我要提這段歷史呢?就是因為現(xiàn)在的 iOS 開發(fā)者實在太舒服了,大部分時候,他們根本都不用關心程序的內(nèi)存管理行為。但是,雖然 ARC 幫我們解決了引用計數(shù)的大部分問題,一些年輕的 iOS 開發(fā)者仍然會做不好內(nèi)存管理工作。他們甚至不能理解常見的循環(huán)引用問題,而這些問題會導致內(nèi)存泄漏,最終使得應用運行緩慢或者被系統(tǒng)終止進程。
所以,我們每一個 iOS 開發(fā)者,需要理解引用計數(shù)這種內(nèi)存管理方式,只有這樣,才能處理好內(nèi)存管理相關的問題。
ARC 出現(xiàn)之前的 MRC 時代
MRC 時期,前輩們是這樣寫 iOS 代碼的
我們先寫好一段 iOS 的代碼,然后屏住呼吸,開始運行它,不出所料,它崩潰了。在 MRC 時代,即使是最牛逼的 iOS 開發(fā)者,也不能保證一次性就寫出完美的內(nèi)存管理代碼。于是,我們開始一步一步調(diào)試,試著打印出每個懷疑對象的引用計數(shù)(Retain Count),然后,我們小心翼翼地插入合理的 retain 和 release 代碼。經(jīng)過一次又一次的應用崩潰和調(diào)試,終于有一次,應用能夠正常運行了!于是我們長舒一口氣,露出久違的微笑。
引用計數(shù)
這里面提到了引用計數(shù),那么什么是引用計數(shù)?
引用計數(shù)(Reference Count)是一個簡單而有效的管理對象生命周期的方式。當我們創(chuàng)建一個新對象的時候,它的引用計數(shù)為 1,當有一個新的指針指向這個對象時,我們將其引用計數(shù)加 1,當某個指針不再指向這個對象是,我們將其引用計數(shù)減 1,當對象的引用計數(shù)變?yōu)?0 時,說明這個對象不再被任何指針指向了,這個時候我們就可以將對象銷毀,回收內(nèi)存。由于引用計數(shù)簡單有效,除了 Objective-C 和 Swift 語言外,微軟的 COM(Component Object Model )、C++11(C++11 提供了基于引用計數(shù)的智能指針 share_prt)等語言也提供了基于引用計數(shù)的內(nèi)存管理方式。
手動管理引用計數(shù)的思考方式:
- 自己生成的對象,自己持有
- 非自己生成的對象,自己也能持有
- 不再需要自己持有的對象時釋放
- 非自己持有的對象無法釋放
有了這種思考方式,我們就生成了對應的 Objective-C 方法來管理引用計數(shù)。
下表是對象操作與 Objective-C 方法的對應
對象操作 | Objective-C 方法 | 引用計數(shù) |
---|---|---|
生成并持有對象 | alloc/new/copy/mutableCopy 等方法 | 引用計數(shù)+1 |
持有對象 | retain | 引用計數(shù) +1 |
釋放對象 | release | 引用計數(shù) -1 |
廢棄對象 | dealloc | 引用計數(shù) -1 |
如圖,可清晰的看到 對象操作與 Objective-C 方法的對應
既然到了這兒,我們也能大概猜到 MRC 下程序員們是如何管理內(nèi)存的了
在 MRC 模式下,所有的對象都需要手動的添加 retain、release 代碼來管理內(nèi)存。使用 MRC ,需要遵守誰創(chuàng)建,誰回收的原則。也就是誰 alloc ,誰 release ;誰 retain ,誰 release。
當引用計數(shù)為0的時候,必須回收,引用計數(shù)不為0,不能回收,如果引用計數(shù)為0,但是沒有回收,會造成內(nèi)存泄露。如果引用計數(shù)為0,繼續(xù)釋放,會造成野指針。為了避免出現(xiàn)野指針,我們在釋放的時候,會先讓指針= nil。
這塊兒先不介紹這幾個方法的底層實現(xiàn),我們只是簡單的通過一段簡單的代碼看看這幾個方式是如何進行內(nèi)存管理的。
我們首先要修改工程設置,給 main.m 加上 -fno-objc-arc 的編譯參數(shù),這個參數(shù)可以啟動手動管理引用計數(shù)的模式。
然后,我們先輸入如下代碼,通過 Log 看到相應的引用計數(shù)的變化。
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *object = [[NSObject alloc] init]; // 引用計數(shù) +1
NSLog(@"Reference Count = %lu", (unsigned long)[object retainCount]);
NSObject *another = [object retain];//引用計數(shù) +1
NSLog(@"Reference Count = %lu", [object retainCount]);
[another release];//引用計數(shù) -1
NSLog(@"Reference Count = %lu", [object retainCount]);
[object release];// 到這兒,引用計數(shù)就為 0 了。
}
return 0;
}
// 打印的結(jié)果為:
2017-05-23 16:11:35.035467+0800 MRC[1148:75832] Reference Count = 1
2017-05-23 16:11:35.041784+0800 MRC[1148:75832] Reference Count = 2
2017-05-23 16:11:35.041806+0800 MRC[1148:75832] Reference Count = 1
為什么需要引用計數(shù)
看完上述代碼,大家可能會覺得,這就是引用計數(shù)啊,這不挺簡單的嗎?但是,我要告訴大家的,上面那段代碼只是非常簡單的例子,我們還看不出來引用計數(shù)真正的用處。因為該對象的生命期只是在一個函數(shù)內(nèi),所以在真實的應用場景下,我們在函數(shù)內(nèi)使用一個臨時的對象,通常是不需要修改它的引用計數(shù)的,只需要在函數(shù)返回前將該對象銷毀即可。
引用計數(shù)真正派上用場的場景在于面向?qū)ο蟮某绦蛟O計架構(gòu)中,用于對象之間傳遞和共享數(shù)據(jù)。
假如對象 A 生成了一個對象 M,需要調(diào)用對象 B 的某一個方法,將對象 M 作為參數(shù)傳遞過去。在沒有引用計數(shù)的情況下,一般內(nèi)存管理的原則是 “誰申請誰釋放”,那么對象 A 就需要在對象 B 不再需要對象 M 的時候,將對象 M 銷毀。但對象 B 可能只是臨時用一下對象 M,也可能覺得對象 M
很重要,將它設置成自己的一個成員變量,那這種情況下,什么時候銷毀對象 M 就成了一個難題。
對于這種情況,我們可以在對象 A 在調(diào)用完對象 B 后直接釋放參數(shù)對象 M, B 在對參數(shù) M 做一個 Copy ,生成另一個對象 M1,B 自己管理 M1 。
還有一種方法就是對象 A 在構(gòu)造完對象 M 之后,始終不銷毀對象 M,由對象 B 來完成對象 M 的銷毀工作。如果對象 B 需要長時間使用對象 M,它就不銷毀它,如果只是臨時用一下,則可以用完后馬上銷毀。如果情況在復雜點,出現(xiàn)個對象 C,那么我們的工作是不是就更復雜了呢。
但是上述兩種方法要么使得工作量大增,影響性能,要么使得對象間的耦合太過緊密,增大復雜性。
所以,這個時候,我們的引用計數(shù)就可以很好的解決這個問題。在參數(shù) M 的傳遞過程中,哪些對象需要長時間使用這個對象,就把它的引用計數(shù)加 1,使用完了之后再把引用計數(shù)減 1。所有對象都遵守這個規(guī)則的話,對象的生命期管理就可以完全交給引用計數(shù)了。我們也可以很方便地享受到共享對象帶來的好處。
ARC 下的內(nèi)存管理
ARC 能夠解決 iOS 開發(fā)中 90% 的內(nèi)存管理問題,但是另外還有 10% 內(nèi)存管理,是需要開發(fā)者自己處理的,這主要就是與底層 Core Foundation 對象交互的那部分,底層的 Core Foundation 對象由于不在 ARC 的管理下,所以需要自己維護這些對象的引用計數(shù)。
這里我們先拋出 ARC 不能解決的問題:
- Block 等引發(fā)的循環(huán)引用問題
- 底層 Core Foundation 對象需要手動管理
所有權(quán)修飾符
ARC 有效時,id 類型和對象類型同 C 語言其他類型不同,其類型上必須附加所有權(quán)修飾符。所有權(quán)修飾符一共有四種。
- _strong 修飾符
- _strong修飾符:id 類型和對象類型默認的所有權(quán)修飾符;它可以保證將這些修飾符的自動變量初始化為nil.
- _strong 修飾符表示對對象的“強引用”; 附有_strong 修飾符的變量之間可以互相賦值。
- 持有強引用的變量在超出其作用域時被廢棄,隨著強引用的失效,引用的對象會隨之消失
- 通過 _strong 修飾符,不必再次鍵入 retain 和 release
{
// ARC 有效時
id obj = [[NSObject alloc] init];//自己生成并持有對象
//因為對象obj 強引用,自己也持有對象
}
<!--//超出作用域,強引用失效,自動釋放自己持有的對象-->
{
// ARC 無效時,該方法與 ARC 有效時一樣
id obj = [[NSObject alloc] init];//自己生成并持有對象
[obj release];// 需要自己調(diào)用 release 方法來釋放
}
- _weak 修飾符
- 弱引用,不持有所指向?qū)ο蟮乃袡?quán)
- 可以避免循環(huán)引用
- 在持有某對象的弱引用時,若該對象被廢棄,則此弱引用將自動失效且處于 nil 被賦值的狀態(tài)。
// 避免循環(huán)引用
__weak __typeof(self) weakSelf = self;
{
// 自己生成并持有對象
id _strong obj0 = [NSObject alloc] init];
// 因為 obj0 變量為強引用,所以自己持有對象
id _weak obj1 = obj0;
// obj1 變量持有生成對象的弱引用
}
/*
* 因為 obj0 變量超出其作用域,強引用失效
* 所以自動釋放自己持有的對象
* 因為對象的所有者不存在,所以廢棄該對象
*/
- _unsafe_unretained 修飾符
- 不安全的所有權(quán)修飾符,附有 _unsafe_unretained 修飾符的變量不屬于編譯器測內(nèi)存管理對象
- 為兼容iOS5以下版本的產(chǎn)物,可以理解成MRC下的weak
- 在使用 _unsafe_unretained 修飾符時,賦值給附有 _strong 修飾符的變量時,要確保被賦值的對象確實存在
- _autoreleasing 修飾符
- 自動釋放對象的引用,一般用于傳遞參數(shù)
- 在 ARC 有效時,用 @autoreleasepool 塊替代 NSAutoreleasePool 類,用附有 _autoreleasing 修飾符的變量替代 autorelease 方法。
- 當沒有顯示指定所有權(quán)修飾符, id obj 和附有 _strong 修飾符 的obj 是完全一樣的。編譯器在對象變量超過作用域時,釋放它并且自動將它注冊到 autoreleasepool 中。
- 使用 _weak 修飾符的變量時,要訪問注冊到 autoreleasepool 的對象
- id 的指針或?qū)ο蟮闹羔樤跊]有顯示指定時會被附加上 _autoreleasing 修飾符
id _weak obj1 = obj0;
NSLog(@"class= %@",[obj1 class]);
上述代碼與以下代碼相同
id _weak obj1 = obj0;
id _autoreleasing tmp = obj1;
NSLog(@"class= %@",[obj1 class]);
autoreleasepool 范圍以塊級源代碼表示,提高了程序的可讀性,所以今后在ARC無效時也推薦使用 @autoreleaseepool 塊。
另外,無論 ARC 是否有效,調(diào)試用的非公開函數(shù) _objc_autoreleasePoolPrint() 都可使用。
_objc_rootRetainCount(obj)
利用這一函數(shù)可有效的幫助我們調(diào)試注冊到 autoreleasepool 上的對象
ARC 的規(guī)則
- 不能使用 retain/release/retainCount/autorelease
- 不能使用 NSAllocateObject/NSDeallocateObject
- 須遵循內(nèi)存管理的方式命名規(guī)則
- 不要顯示調(diào)用 dealloc
- 使用 @autorealeasepool 塊代替 NSAutoreleasePool
- 不要使用區(qū)域(NSZone)
- 對象型變量不能作為 C 語言結(jié)構(gòu)體的成員
- 顯示轉(zhuǎn)換 'id' 和 'void'
循環(huán)引用問題
簡單的來說循環(huán)引用就是對象 A 和對象 B,相互引用了對方作為自己的成員變量,只有當自己銷毀時,才會將成員變量的引用計數(shù)減 1。因為對象 A 的銷毀依賴于對象 B 銷毀,而對象 B 的銷毀與依賴于對象 A 的銷毀,這樣就造成了我們稱之為循環(huán)引用(Reference Cycle)的問題,這兩個對象即使在外界已經(jīng)沒有任何指針能夠訪問到它們了,它們也無法被釋放。實際上,多個對象依次持有對方,形式一個環(huán)狀,也可以造成循環(huán)引用問題,而且在真實編程環(huán)境中,環(huán)越大就越難被發(fā)現(xiàn)。
- 解決循環(huán)引用的問題的兩個方法
-
知道會產(chǎn)生循環(huán)引用,在合理的位置主動斷開環(huán)中的一個引用,使得對象得以回收
主動斷開循環(huán)引用這種方式常見于各種與 block 相關的代碼邏輯中。
但是主動斷開循環(huán)引用這種操作依賴于程序員自己手工顯式地控制,相當于回到了以前 “誰申請誰釋放” 的內(nèi)存管理年代,它依賴于程序員自己有能力發(fā)現(xiàn)循環(huán)引用并且知道在什么時機斷開循環(huán)引用回收內(nèi)存(這通常與具體的業(yè)務邏輯相關)
- 常見的辦法是使用弱引用 (weak reference) 的辦法,弱引用雖然持有對象,但是并不增加引用計數(shù),這樣就避免了循環(huán)引用的產(chǎn)生。在 iOS 開發(fā)中,弱引用通常在 delegate 模式中使用。
- 使用 Xcode 檢測循環(huán)引用
Core Foundation 對象的內(nèi)存管理
Core Foundation 對象主要使用在用 C語言編寫的 Core Foundation 框架中,并使用引用計數(shù)的對象;在 ARC 無效時 ,Core Foundation 框架中的 retain/release 分別是 CFRetain/CFRelease;因為 Core Foundation 對象和 Objective-C 對象沒有什么區(qū)別,所以在 ARC 無效時,可以使用簡單的 C 語言就可以實現(xiàn)互換。
在 ARC 下,我們有時需要將一個 Core Foundation 對象轉(zhuǎn)換成一個 Objective-C 對象,這個時候我們需要告訴編譯器,轉(zhuǎn)換過程中的引用計數(shù)需要做如何的調(diào)整。這就引入了 bridge 相關的關鍵字,以下是這些關鍵字的說明:
- ==__bridge== : 只做類型轉(zhuǎn)換,不修改相關對象的引用計數(shù),原來的 Core Foundation 對象在不用時,需要調(diào)用 CFRelease 方法。
- ==__bridge_retained== :類型轉(zhuǎn)換后,將相關對象的引用計數(shù)加 1,原來的 Core Foundation 對象在不用時,需要調(diào)用 CFRelease 方法。
- ==__bridge_transfer== :類型轉(zhuǎn)換后,將該對象的引用計數(shù)交給 ARC 管理,Core Foundation 對象在不用時,不再需要調(diào)用 CFRelease 方法。
總結(jié)
這篇文章并沒有涉及 MRC 以及 ARC 實現(xiàn)的底層,所涉及到的知識也是個人看完 高級編程第一章的知識以及 唐巧大神的文章后,自己總結(jié)的筆記。在之后的探索中,也會從底層出發(fā)來剖析內(nèi)存管理的知識。
參考博客:唐巧的理解 iOS 內(nèi)存管理