深刻理解 iOS 內存管理

本文探討了在iOS中內存管理的命名規則、引用計數的實現機制以及 weak 變量的內部實現。git

本文同時發表於個人我的博客程序員

本文首次發表於2015年,雖然如今 MRC 使用的不多,但 ARC 與 MRC 並無本質上的區別,瞭解其背後的原理十分重要。github

Overview


Memory Management 在 C 語言體系中一直是個重要的話題,它們沒有像 Java 那樣的 Garbage Collection,內存管理徹底由程序員負責。所以,稍有不慎就會出現 Memory Leak 或 Dangling Pointer 等嚴重問題,這對於程序來講是致命的!面試

Objective-C 做爲在 C 語言基礎上發展起來的面嚮對象語言,自身天然也沒有內存管理機制。所以,做爲 iOS 程序員的咱們也須要當心翼翼地處理着內存問題。然而,這一切隨着 ARC 的到來有很大的改觀。編程

由 iOS5 和 Xcode4.2 內置的編譯器 LLVM3.0 共同支持的 ARC(Automatic Reference Counting),如其名稱所示實現了內存的自動管理。簡單地說,其實質就是將內存管理的工做由程序員轉交給編譯器來完成,固然某些特性須要 runtime 的支持。數組

內存管理中的命名規則


與 ARC 相比,咱們將手動內存管理機制稱做 MRC(Mannul Reference Counting),就像 ARC 與 MRC 名稱所展現的那樣,二者從內存管理的本質上講沒有區別,都是經過引用計數(Reference Counting)機制管理內存。不一樣的是,在 ARC 中內存管理相關的代碼由編譯器在編譯代碼時自動插入。性能優化

那麼問題來了~bash

ARC 下編譯器如何自動插入內存管理代碼?更直白點,編譯器如何知道在某處須要插入相關的代碼?app

首先,能想到的是在類的 dealloc 方法中,對該類的實例對象所持有的成員變量(strong)執行 release 操做。ide

那麼,對於局部變量,編譯器如何管理內存?

{
    NSString *str = [[NSString alloc] initWithFormat:@"test ARC"];
    NSLog(@"%@", str);
}
複製代碼

咱們知道,在 ARC 下,上面的代碼片斷會被編譯器處理成(僅是示例,編譯器最終的處理可能不徹底一致):

{
    NSString * str = [[NSString alloc] initWithFormat:@"test ARC"];
    NSLog(@"%@", str);
    [str release];			// 編譯器插入了 release
}
複製代碼

而下面的代碼片斷,編譯器沒有爲內存管理添加任何代碼:

{
    NSString *str = [NSString stringWithFormat:@"test ARC"];
    NSLog(@"%@", str);
}
複製代碼

爲何編譯器在處理上述兩段代碼時,採起不一樣的態度?

答案很明顯,代碼2返回的是 autorelese 對象,其已被歸入內存管理之中,故不須要編譯器再做處理,而代碼1卻沒有。

嗯,問題彷佛已獲得完美的解答! 然而,這一切都是站在人的角度去分析的,編譯器如何知道代碼1須要其管理內存,而代碼2不須要?

ok,這就是本節主題:『命名規則』要解決的問題。

任何如下列名稱爲前綴的方法,若其返回值爲 object,則方法調用者持有該 object:

  • alloc
  • new
  • copy
  • mutableCopy

還有一個更爲嚴格的規則:任何以 init 爲前綴的方法必須遵照下列規則:

  • 該方法必須是實例方法;
  • 該方法必須返回類型爲id或其所屬class、superclass、subclass 的對象;
  • 該方法返回的 object 不能是 autorelese,即方法調用者持有返回的 object。

『方法調用者持有該 object』也就意味着該 object 的內存問題須要調用方管理。

在此以外的任何方法返回的 object,其調用方都不持有,即返回的應該是 autorelease object。

例外:以 allocate、newer、copying、mutableCopyed 爲前綴的方法以及 initialize 方法不在上述規則以內。

編譯器根據上述規則,很容易就能判斷出在代碼1中須要處理對象str的內存問題,而代碼2返回的是 autorelease object,故不須要其處理。

在編碼過程當中,應該嚴格按照上述規則執行!

那問題來了,若是在編碼過程當中硬是不遵照上述規則如何? 首先,你就被排除在優秀程序員以外了!MRC 時代,不按上述規則寫出的代碼在內存管理上極難維護,很容易出現內存泄漏或屢次釋放的問題。

在 MRC 下,上述代碼經過 Analyze 作靜態分析時會給出如上圖所示的 warning。

ARC 與 MRC 混用

因爲不少項目經歷了 MRC 到 ARC 的時代,所以 ARC 與 MRC 在項目中同時存在的狀況大有所在。 若是,此時不遵照上述命名規則會出現問題嗎?

運行結果:
crash!

緣由在於在 ARC 的文件中調用了以 alloc 爲前綴的方法,根據命名規則,此時編譯器認爲 allocString 方法將返回一個須要其管理內存(release)的對象。上述 ARC 的代碼將被編譯器處理爲:

然而,在 MRC 中的 allocString方法並未遵照命名規則,其返回的是一個 autorelease object。最終效果就是 release 了一個 autorelease object!這類問題,咱們除了知道出問題對象的類型,crash 堆棧沒有任何幫助,所以在大型項目中排查此類問題有必定的難度(工做量)。
那麼,在 ARC 的文件中不遵照命名規則會出問題嗎?
這段代碼運行正常沒有 crash,緣由在於 allocString 方法自己及調用者都在 ARC 管理範疇之類,編譯器很清楚該如何處理。即使如此,做爲優秀的程序員,在平常 coding 過程當中仍是應該遵照命名規則。

Inside Reference Counting


前文已提到,不管是 ARC 仍是 MRC,其本質都是經過引用計數(Reference Counting)來管理內存。

若是讓你設計一套引用計數機制,你會怎麼作? 嗯,這是個不錯的面試題! 其實,該問題的答案不外乎兩種:

  • 在對象內部管理引用計數;
  • 經過外部結構(如:hash 表)統一管理引用計數。

GNUstep’s Implementation of Reference Counting

GUNstep 實現了一套兼容 Cocoa Framework 的 Framework,做爲開源代碼咱們來看看它是如何處理引用計數的:

經過整理,刪除非必要的代碼,GUNstep 實現的 alloc方法如上所示。能夠看到,其使用了一個結構體 obj_layout來保存引用計數,同時該結構體被附在所生成 object 的頭部。 object內存佈局以下圖所示(引自《Objective-C高級編程》):

Apple’s Implementation of Reference Counting

因爲 Apple 現已來源了相關的代碼,使得咱們能夠進一步一探究竟。Apple 全部的來源代碼均可以在此找到:Apple Opensource

首先,咱們來看看 Apple 是如何實現 retain 方法的:

若是拋開 Apple 所作的優化,其 retain方法最終會調用上圖所示的 sidetable_retain方法。

在繼續以前有必要介紹一下新朋友:SideTable

在類 SideTable的成員變量中,彷佛看到了熟悉的味道!是的,沒錯,其中的 RefcountMap 就是引用計數表,而 weak_table_t則是弱引用表(weak table).

RefcountMap 則是一個簡單的 map,其 key 爲 object 內存地址,value 爲引用計數值。

經過SideTable源碼,還能夠得出以下結論:

  • 存在全局的若干個SideTable實例,它們保存在 static 成員變量table_buf中;[在 iOS 平臺上有8個這樣的實例(SIDE_TABLE_STRIPE = 8)]
  • 程序運行過程當中生成的全部對象都會經過其內存地址映射到table_buf中相應的SideTable實例上。

這裏之因此會存在多個SideTable實例,object 映射到不一樣SideTable實例上,猜想是出於性能優化的目的,避免SideTable中的 reference table、weak table 過大。

回到上面的sidetable_retain方法,其首先經過 object 的地址找到對應的 sidetale,而後經過 RefcountMap將該 object 的引用計數加1.

releaseretainCount等相關方法的代碼在該開源代碼中也能找到,在此再也不贅述。

簡單地說,Apple 經過全局的 map 來記錄Reference Counting,其key 爲 object 地址,value 爲引用計數值。

那麼,GNUstep 與 Apple 的實現方案各有什麼優劣點? GNUstep 的方案從實現的角度看簡單明瞭,而 Apple 的方案可以更好的把控系統內存使用狀況,對調試有必定的幫助。

Inside Weak


weak 無疑是 ARC 送給咱們的一大利器,經過它基本能消滅 delegate 引發的 dangling pointer 問題。這得益於 weak 指針指向的 object 在 dealloc 時,該指針會被置爲 nil。 那麼,系統是如何處理 weak 變量的呢?

上面這個簡單的代碼片斷會被編譯器處理成以下所示的 pseudo code:
注:如下代碼已通過整理以便閱讀、抓住重點。

經過上述代碼能夠看到,對於 weak 變量,系統將以 weak 指針變量的地址(&weakNum)、用於賦值的 object(num)爲參數調用objc_initWeak方法:

objc_initWeak方法進一步調用 objc_storeWeak方法,在該方法中,以賦值object(num)對應的 sidetable 中的 weaktable、賦值object(num)以及 weak 變量的指針爲參數調用 weak_register_no_lock方法。 在 objc_storeWeak方法中,還能夠看到,對於 weak 引用會在賦值object的引用計數表中設置弱引用標誌位(SIDE_TABLE_WEAKLY_REFERENCED),具體緣由有待深究。 固然,在 objc_storeWeak方法中作的最後一件事情就是將賦值對象的地址賦給 weak 指針。
weak_register_no_lock方法首先檢查賦值object在 weak table 中是否存在相應的條目,若存在則直接在其中添加該 weak 變量的信息,若不存在則插入賦值 object 對應的條目。

weaktable

談到 weak,天然少不了要說到 weaktable:

在 weaktable 中,最重要的成員莫過於 weak_entry_t類型的數組:weak_entries。在 weak_entry_t結構體中,包含賦值 object 的指針以及全部指向該賦值 object 的 weak 變量列表(weak_referrer_t *referrers)。

那麼,在 weaktable 中如何經過object 找到其對應的 entry?

最後一個問題,在 weak 指針所指向的 object 被 dealloc 時,weak 指針會被置爲 nil。那麼問題來了,若是 weak 變量的生命週期在其指向的 object 以前就結束了,會如何?

若在 weak 變量生命週期結束後,其所使用的內存塊被從新利用賦上了新值,而此時上述 object 被 dealloc,若根據 weak table 中的條目將其對應的 weak 變量一一置爲 nil,則上述被從新利用的變量也將被清0,這顯然是不合適的。

在上面提到的 pseudo code 中,咱們看到在 weak 變量 weakNum 生命週期結束時,調用了objc_destroyWeak方法,沒錯,該方法就是用於解決上述問題的。 objc_destroyWeak方法最終會調用weak_unregister_no_lock方法,其會將 weak 變量從 weak table 中移除掉。

Toll-Free Bridge cast


__bridge cast

在 ARC 下,id 與 void*之間再也不像 MRC 時代能夠任意轉換,在 ARC 下若需在二者之間轉換可使用__bridge cast。 在使用時需注意,在將 id 轉換爲 void*時,其再也不在 ARC 的內存管理範疇內,極有可能出現dangling pointer。

__bridge_retained cast

__bridge_retained的做用是使得被賦值變量持有賦值 object。

上述 ARC 代碼與下面的 MRC 代碼在內存管理上是等價的

__bridge_transfer cast

__bridge_transfer的做用是使得賦值 object 在賦值後被 release。

上面兩段代碼是等價的,正如__bridge_transfer名稱所示,其做用是將持有權從賦值 object 轉到被賦值變量。

在 ARC 下,須要轉換的狀況發生在Objective-C object 與 Core Foundation object 間,此時可使用如下方法:

小結


關於內存管理有不少的問題值得探討,隨着 Apple 相關代碼的開源,一切的私密都能在源碼中找到答案。

相關文章
相關標籤/搜索