理解 iOS 的內存管理

引言:

ARC的出生及成長背景

蘋果在 2011 年的時候,在 WWDC 大會上提出了自動的引用計數(ARC)。ARC 背后的原理是依賴編譯器的靜態分析能力,通過在編譯時找出合理的插入引用計數管理代碼,從而徹底解放程序員。

在 ARC 剛剛出來的時候,業界對此黑科技充滿了懷疑和觀望,加上現有的 MRC 代碼要做遷移本來也需要額外的成本,所以 ARC 并沒有被很快接受。直到 2013 年左右,蘋果認為 ARC 技術足夠成熟,直接將 macOS(當時叫 OS X)上的垃圾回收機制廢棄,從而使得 ARC 迅速被接受。

2014 年的 WWDC 大會上,蘋果推出了 Swift 語言,而該語言仍然使用 ARC 技術,作為其內存管理方式。

以下是引用唐巧大神的話:

為什么我要提這段歷史呢?就是因為現在的 iOS 開發者實在太舒服了,大部分時候,他們根本都不用關心程序的內存管理行為。但是,雖然 ARC 幫我們解決了引用計數的大部分問題,一些年輕的 iOS 開發者仍然會做不好內存管理工作。他們甚至不能理解常見的循環引用問題,而這些問題會導致內存泄漏,最終使得應用運行緩慢或者被系統終止進程。

所以,我們每一個 iOS 開發者,需要理解引用計數這種內存管理方式,只有這樣,才能處理好內存管理相關的問題。

ARC 出現之前的 MRC 時代

MRC 時期,前輩們是這樣寫 iOS 代碼的

我們先寫好一段 iOS 的代碼,然后屏住呼吸,開始運行它,不出所料,它崩潰了。在 MRC 時代,即使是最牛逼的 iOS 開發者,也不能保證一次性就寫出完美的內存管理代碼。于是,我們開始一步一步調試,試著打印出每個懷疑對象的引用計數(Retain Count),然后,我們小心翼翼地插入合理的 retain 和 release 代碼。經過一次又一次的應用崩潰和調試,終于有一次,應用能夠正常運行了!于是我們長舒一口氣,露出久違的微笑。

引用計數

這里面提到了引用計數,那么什么是引用計數?

引用計數(Reference Count)是一個簡單而有效的管理對象生命周期的方式。當我們創建一個新對象的時候,它的引用計數為 1,當有一個新的指針指向這個對象時,我們將其引用計數加 1,當某個指針不再指向這個對象是,我們將其引用計數減 1,當對象的引用計數變為 0 時,說明這個對象不再被任何指針指向了,這個時候我們就可以將對象銷毀,回收內存。由于引用計數簡單有效,除了 Objective-C 和 Swift 語言外,微軟的 COM(Component Object Model )、C++11(C++11 提供了基于引用計數的智能指針 share_prt)等語言也提供了基于引用計數的內存管理方式。

手動管理引用計數的思考方式:

  • 自己生成的對象,自己持有
  • 非自己生成的對象,自己也能持有
  • 不再需要自己持有的對象時釋放
  • 非自己持有的對象無法釋放

有了這種思考方式,我們就生成了對應的 Objective-C 方法來管理引用計數。
下表是對象操作與 Objective-C 方法的對應

對象操作 Objective-C 方法 引用計數
生成并持有對象 alloc/new/copy/mutableCopy 等方法 引用計數+1
持有對象 retain 引用計數 +1
釋放對象 release 引用計數 -1
廢棄對象 dealloc 引用計數 -1

如圖,可清晰的看到 對象操作與 Objective-C 方法的對應

對應關系

既然到了這兒,我們也能大概猜到 MRC 下程序員們是如何管理內存的了

在 MRC 模式下,所有的對象都需要手動的添加 retain、release 代碼來管理內存。使用 MRC ,需要遵守誰創建,誰回收的原則。也就是誰 alloc ,誰 release ;誰 retain ,誰 release。
當引用計數為0的時候,必須回收,引用計數不為0,不能回收,如果引用計數為0,但是沒有回收,會造成內存泄露。如果引用計數為0,繼續釋放,會造成野指針。為了避免出現野指針,我們在釋放的時候,會先讓指針= nil。

這塊兒先不介紹這幾個方法的底層實現,我們只是簡單的通過一段簡單的代碼看看這幾個方式是如何進行內存管理的。

我們首先要修改工程設置,給 main.m 加上 -fno-objc-arc 的編譯參數,這個參數可以啟動手動管理引用計數的模式。
然后,我們先輸入如下代碼,通過 Log 看到相應的引用計數的變化。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *object = [[NSObject alloc] init]; // 引用計數 +1
        NSLog(@"Reference Count = %lu", (unsigned long)[object retainCount]);
        NSObject *another = [object retain];//引用計數 +1
        NSLog(@"Reference Count = %lu", [object retainCount]);
        [another release];//引用計數 -1
        NSLog(@"Reference Count = %lu", [object retainCount]);
        [object release];// 到這兒,引用計數就為 0 了。
        
    }
    return 0;
}
// 打印的結果為:
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

為什么需要引用計數

看完上述代碼,大家可能會覺得,這就是引用計數啊,這不挺簡單的嗎?但是,我要告訴大家的,上面那段代碼只是非常簡單的例子,我們還看不出來引用計數真正的用處。因為該對象的生命期只是在一個函數內,所以在真實的應用場景下,我們在函數內使用一個臨時的對象,通常是不需要修改它的引用計數的,只需要在函數返回前將該對象銷毀即可。

引用計數真正派上用場的場景在于面向對象的程序設計架構中,用于對象之間傳遞和共享數據

假如對象 A 生成了一個對象 M,需要調用對象 B 的某一個方法,將對象 M 作為參數傳遞過去。在沒有引用計數的情況下,一般內存管理的原則是 “誰申請誰釋放”,那么對象 A 就需要在對象 B 不再需要對象 M 的時候,將對象 M 銷毀。但對象 B 可能只是臨時用一下對象 M,也可能覺得對象 M
很重要,將它設置成自己的一個成員變量,那這種情況下,什么時候銷毀對象 M 就成了一個難題。


對于這種情況,我們可以在對象 A 在調用完對象 B 后直接釋放參數對象 M, B 在對參數 M 做一個 Copy ,生成另一個對象 M1,B 自己管理 M1 。


還有一種方法就是對象 A 在構造完對象 M 之后,始終不銷毀對象 M,由對象 B 來完成對象 M 的銷毀工作。如果對象 B 需要長時間使用對象 M,它就不銷毀它,如果只是臨時用一下,則可以用完后馬上銷毀。如果情況在復雜點,出現個對象 C,那么我們的工作是不是就更復雜了呢。


但是上述兩種方法要么使得工作量大增,影響性能,要么使得對象間的耦合太過緊密,增大復雜性。

所以,這個時候,我們的引用計數就可以很好的解決這個問題。在參數 M 的傳遞過程中,哪些對象需要長時間使用這個對象,就把它的引用計數加 1,使用完了之后再把引用計數減 1。所有對象都遵守這個規則的話,對象的生命期管理就可以完全交給引用計數了。我們也可以很方便地享受到共享對象帶來的好處。

ARC 下的內存管理

ARC 能夠解決 iOS 開發中 90% 的內存管理問題,但是另外還有 10% 內存管理,是需要開發者自己處理的,這主要就是與底層 Core Foundation 對象交互的那部分,底層的 Core Foundation 對象由于不在 ARC 的管理下,所以需要自己維護這些對象的引用計數。

這里我們先拋出 ARC 不能解決的問題:

  • Block 等引發的循環引用問題
  • 底層 Core Foundation 對象需要手動管理

所有權修飾符

ARC 有效時,id 類型和對象類型同 C 語言其他類型不同,其類型上必須附加所有權修飾符。所有權修飾符一共有四種。

  • _strong 修飾符
  • _strong修飾符:id 類型和對象類型默認的所有權修飾符;它可以保證將這些修飾符的自動變量初始化為nil.
  • _strong 修飾符表示對對象的“強引用”; 附有_strong 修飾符的變量之間可以互相賦值。
  • 持有強引用的變量在超出其作用域時被廢棄,隨著強引用的失效,引用的對象會隨之消失
  • 通過 _strong 修飾符,不必再次鍵入 retain 和 release
{
    //  ARC 有效時
    id obj = [[NSObject alloc] init];//自己生成并持有對象
    //因為對象obj 強引用,自己也持有對象
}
  <!--//超出作用域,強引用失效,自動釋放自己持有的對象-->
{
    //  ARC 無效時,該方法與 ARC 有效時一樣
    id obj = [[NSObject alloc] init];//自己生成并持有對象
    [obj release];// 需要自己調用 release 方法來釋放
}
  • _weak 修飾符
  • 弱引用,不持有所指向對象的所有權
  • 可以避免循環引用
  • 在持有某對象的弱引用時,若該對象被廢棄,則此弱引用將自動失效且處于 nil 被賦值的狀態。
// 避免循環引用
__weak __typeof(self) weakSelf = self;
{
    // 自己生成并持有對象
    id _strong obj0 = [NSObject alloc] init];
    // 因為 obj0 變量為強引用,所以自己持有對象
    id _weak obj1 = obj0;
    // obj1 變量持有生成對象的弱引用
}
/*
* 因為 obj0 變量超出其作用域,強引用失效
* 所以自動釋放自己持有的對象
* 因為對象的所有者不存在,所以廢棄該對象
 */
  • _unsafe_unretained 修飾符
  • 不安全的所有權修飾符,附有 _unsafe_unretained 修飾符的變量不屬于編譯器測內存管理對象
  • 為兼容iOS5以下版本的產物,可以理解成MRC下的weak
  • 在使用 _unsafe_unretained 修飾符時,賦值給附有 _strong 修飾符的變量時,要確保被賦值的對象確實存在
  • _autoreleasing 修飾符
  • 自動釋放對象的引用,一般用于傳遞參數
  • 在 ARC 有效時,用 @autoreleasepool 塊替代 NSAutoreleasePool 類,用附有 _autoreleasing 修飾符的變量替代 autorelease 方法。
  • 當沒有顯示指定所有權修飾符, id obj 和附有 _strong 修飾符 的obj 是完全一樣的。編譯器在對象變量超過作用域時,釋放它并且自動將它注冊到 autoreleasepool 中。
  • 使用 _weak 修飾符的變量時,要訪問注冊到 autoreleasepool 的對象
  • id 的指針或對象的指針在沒有顯示指定時會被附加上 _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 是否有效,調試用的非公開函數 _objc_autoreleasePoolPrint() 都可使用。
_objc_rootRetainCount(obj)
利用這一函數可有效的幫助我們調試注冊到 autoreleasepool 上的對象

ARC 的規則

  • 不能使用 retain/release/retainCount/autorelease
  • 不能使用 NSAllocateObject/NSDeallocateObject
  • 須遵循內存管理的方式命名規則
  • 不要顯示調用 dealloc
  • 使用 @autorealeasepool 塊代替 NSAutoreleasePool
  • 不要使用區域(NSZone)
  • 對象型變量不能作為 C 語言結構體的成員
  • 顯示轉換 'id' 和 'void'

循環引用問題

簡單的來說循環引用就是對象 A 和對象 B,相互引用了對方作為自己的成員變量,只有當自己銷毀時,才會將成員變量的引用計數減 1。因為對象 A 的銷毀依賴于對象 B 銷毀,而對象 B 的銷毀與依賴于對象 A 的銷毀,這樣就造成了我們稱之為循環引用(Reference Cycle)的問題,這兩個對象即使在外界已經沒有任何指針能夠訪問到它們了,它們也無法被釋放。實際上,多個對象依次持有對方,形式一個環狀,也可以造成循環引用問題,而且在真實編程環境中,環越大就越難被發現。

  • 解決循環引用的問題的兩個方法
  1. 知道會產生循環引用,在合理的位置主動斷開環中的一個引用,使得對象得以回收

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

  1. 常見的辦法是使用弱引用 (weak reference) 的辦法,弱引用雖然持有對象,但是并不增加引用計數,這樣就避免了循環引用的產生。在 iOS 開發中,弱引用通常在 delegate 模式中使用。
  • 使用 Xcode 檢測循環引用

Core Foundation 對象的內存管理

Core Foundation 對象主要使用在用 C語言編寫的 Core Foundation 框架中,并使用引用計數的對象;在 ARC 無效時 ,Core Foundation 框架中的 retain/release 分別是 CFRetain/CFRelease;因為 Core Foundation 對象和 Objective-C 對象沒有什么區別,所以在 ARC 無效時,可以使用簡單的 C 語言就可以實現互換。

在 ARC 下,我們有時需要將一個 Core Foundation 對象轉換成一個 Objective-C 對象,這個時候我們需要告訴編譯器,轉換過程中的引用計數需要做如何的調整。這就引入了 bridge 相關的關鍵字,以下是這些關鍵字的說明:

  • ==__bridge== : 只做類型轉換,不修改相關對象的引用計數,原來的 Core Foundation 對象在不用時,需要調用 CFRelease 方法。
  • ==__bridge_retained== :類型轉換后,將相關對象的引用計數加 1,原來的 Core Foundation 對象在不用時,需要調用 CFRelease 方法。
  • ==__bridge_transfer== :類型轉換后,將該對象的引用計數交給 ARC 管理,Core Foundation 對象在不用時,不再需要調用 CFRelease 方法。

總結

這篇文章并沒有涉及 MRC 以及 ARC 實現的底層,所涉及到的知識也是個人看完 高級編程第一章的知識以及 唐巧大神的文章后,自己總結的筆記。在之后的探索中,也會從底層出發來剖析內存管理的知識。

參考博客:唐巧的理解 iOS 內存管理

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

推薦閱讀更多精彩內容