理解 iOS 的內(nèi)存管理

引言:

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)引用的問題的兩個方法
  1. 知道會產(chǎn)生循環(huán)引用,在合理的位置主動斷開環(huán)中的一個引用,使得對象得以回收

主動斷開循環(huán)引用這種方式常見于各種與 block 相關的代碼邏輯中。
但是主動斷開循環(huán)引用這種操作依賴于程序員自己手工顯式地控制,相當于回到了以前 “誰申請誰釋放” 的內(nèi)存管理年代,它依賴于程序員自己有能力發(fā)現(xiàn)循環(huán)引用并且知道在什么時機斷開循環(huán)引用回收內(nèi)存(這通常與具體的業(yè)務邏輯相關)

  1. 常見的辦法是使用弱引用 (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)存管理

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內(nèi)容