本文探討了在iOS中內存管理的命名規則、引用計數的實現機制以及 weak 變量的內部實現。git
本文同時發表於個人我的博客程序員
本文首次發表於2015年,雖然如今 MRC 使用的不多,但 ARC 與 MRC 並無本質上的區別,瞭解其背後的原理十分重要。github
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:
還有一個更爲嚴格的規則:任何以 init 爲前綴的方法必須遵照下列規則:
id
或其所屬class、superclass、subclass 的對象;『方法調用者持有該 object』也就意味着該 object 的內存問題須要調用方管理。
在此以外的任何方法返回的 object,其調用方都不持有,即返回的應該是 autorelease object。
例外:以 allocate、newer、copying、mutableCopyed 爲前綴的方法以及 initialize 方法不在上述規則以內。
編譯器根據上述規則,很容易就能判斷出在代碼1中須要處理對象str
的內存問題,而代碼2返回的是 autorelease object,故不須要其處理。
在編碼過程當中,應該嚴格按照上述規則執行!
那問題來了,若是在編碼過程當中硬是不遵照上述規則如何? 首先,你就被排除在優秀程序員以外了!MRC 時代,不按上述規則寫出的代碼在內存管理上極難維護,很容易出現內存泄漏或屢次釋放的問題。
在 MRC 下,上述代碼經過 Analyze 作靜態分析時會給出如上圖所示的 warning。因爲不少項目經歷了 MRC 到 ARC 的時代,所以 ARC 與 MRC 在項目中同時存在的狀況大有所在。 若是,此時不遵照上述命名規則會出現問題嗎?
運行結果: crash!緣由在於在 ARC 的文件中調用了以 alloc
爲前綴的方法,根據命名規則,此時編譯器認爲 allocString
方法將返回一個須要其管理內存(release)的對象。上述 ARC 的代碼將被編譯器處理爲:
allocString
方法並未遵照命名規則,其返回的是一個 autorelease object。最終效果就是 release 了一個 autorelease object!這類問題,咱們除了知道出問題對象的類型,crash 堆棧沒有任何幫助,所以在大型項目中排查此類問題有必定的難度(工做量)。
那麼,在 ARC 的文件中不遵照命名規則會出問題嗎?
這段代碼運行正常沒有 crash,緣由在於
allocString
方法自己及調用者都在 ARC 管理範疇之類,編譯器很清楚該如何處理。即使如此,做爲優秀的程序員,在平常 coding 過程當中仍是應該遵照命名規則。
前文已提到,不管是 ARC 仍是 MRC,其本質都是經過引用計數(Reference Counting)來管理內存。
若是讓你設計一套引用計數機制,你會怎麼作? 嗯,這是個不錯的面試題! 其實,該問題的答案不外乎兩種:
GUNstep 實現了一套兼容 Cocoa Framework 的 Framework,做爲開源代碼咱們來看看它是如何處理引用計數的:
經過整理,刪除非必要的代碼,GUNstep 實現的alloc
方法如上所示。能夠看到,其使用了一個結構體
obj_layout
來保存引用計數,同時該結構體被附在所生成 object 的頭部。 object內存佈局以下圖所示(引自《Objective-C高級編程》):
因爲 Apple 現已來源了相關的代碼,使得咱們能夠進一步一探究竟。Apple 全部的來源代碼均可以在此找到:Apple Opensource。
首先,咱們來看看 Apple 是如何實現 retain
方法的:
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.
release
、retainCount
等相關方法的代碼在該開源代碼中也能找到,在此再也不贅述。
簡單地說,Apple 經過全局的 map 來記錄Reference Counting,其key 爲 object 地址,value 爲引用計數值。
那麼,GNUstep 與 Apple 的實現方案各有什麼優劣點? GNUstep 的方案從實現的角度看簡單明瞭,而 Apple 的方案可以更好的把控系統內存使用狀況,對調試有必定的幫助。
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 對應的條目。
談到 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 中移除掉。
在 ARC 下,id 與 void*
之間再也不像 MRC 時代能夠任意轉換,在 ARC 下若需在二者之間轉換可使用__bridge cast
。 在使用時需注意,在將 id 轉換爲 void*
時,其再也不在 ARC 的內存管理範疇內,極有可能出現dangling pointer。
__bridge_retained的做用是使得被賦值變量持有賦值 object。
上述 ARC 代碼與下面的 MRC 代碼在內存管理上是等價的__bridge_transfer的做用是使得賦值 object 在賦值後被 release。
上面兩段代碼是等價的,正如__bridge_transfer名稱所示,其做用是將持有權從賦值 object 轉到被賦值變量。在 ARC 下,須要轉換的狀況發生在Objective-C object 與 Core Foundation object 間,此時可使用如下方法:
關於內存管理有不少的問題值得探討,隨着 Apple 相關代碼的開源,一切的私密都能在源碼中找到答案。