那些經歷過手工管理內存(MRC)時代的人們,必定對 iOS 開發中的內存管理記憶猶新。那個時候大約是 2010 年,國內 iOS 開發剛剛興起,tinyfool 大叔的大名已經如雷貫耳,而我仍是一個默默無聞的剛畢業的小子。那個時候的 iOS 開發過程是這樣的:html
咱們先寫好一段 iOS 的代碼,而後屏住呼吸,開始運行它,不出所料,它崩潰了。在 MRC 時代,即便是最牛逼的 iOS 開發者,也不能保證一次性就寫出完美的內存管理代碼。因而,咱們開始一步一步調試,試着打印出每一個懷疑對象的引用計數(Retain Count),而後,咱們當心翼翼地插入合理的
retain
和release
代碼。通過一次又一次的應用崩潰和調試,終於有一次,應用可以正常運行了!因而咱們長舒一口氣,露出久違的微笑。ios
是的,這就是那個年代的 iOS 開發者,一般狀況下,咱們在開發完一個功能後,須要再花好幾個小時,才能把引用計數管理好。git
蘋果在 2011 年的時候,在 WWDC 大會上提出了自動的引用計數(ARC)。ARC 背後的原理是依賴編譯器的靜態分析能力,經過在編譯時找出合理的插入引用計數管理代碼,從而完全解放程序員。程序員
在 ARC 剛剛出來的時候,業界對此黑科技充滿了懷疑和觀望,加上現有的 MRC 代碼要作遷移原本也須要額外的成本,因此 ARC 並無被很快接受。直到 2013 年左右,蘋果認爲 ARC 技術足夠成熟,直接將 macOS(當時叫 OS X)上的垃圾回收機制廢棄,從而使得 ARC 迅速被接受。github
2014 年的 WWDC 大會上,蘋果推出了 Swift 語言,而該語言仍然使用 ARC 技術,做爲其內存管理方式。編程
爲何我要提這段歷史呢?就是由於如今的 iOS 開發者實在太舒服了,大部分時候,他們根本都不用關心程序的內存管理行爲。可是,雖然 ARC 幫咱們解決了引用計數的大部分問題,一些年輕的 iOS 開發者仍然會作很差內存管理工做。他們甚至不能理解常見的循環引用問題,而這些問題會致使內存泄漏,最終使得應用運行緩慢或者被系統終止進程。網絡
因此,咱們每個 iOS 開發者,須要理解引用計數這種內存管理方式,只有這樣,才能處理好內存管理相關的問題。架構
引用計數(Reference Count)是一個簡單而有效的管理對象生命週期的方式。當咱們建立一個新對象的時候,它的引用計數爲 1,當有一個新的指針指向這個對象時,咱們將其引用計數加 1,當某個指針再也不指向這個對象是,咱們將其引用計數減 1,當對象的引用計數變爲 0 時,說明這個對象再也不被任何指針指向了,這個時候咱們就能夠將對象銷燬,回收內存。因爲引用計數簡單有效,除了 Objective-C 和 Swift 語言外,微軟的 COM(Component Object Model )、C++11(C++11 提供了基於引用計數的智能指針 share_prt)等語言也提供了基於引用計數的內存管理方式。app
爲了更形象一些,咱們再來看一段 Objective-C 的代碼。新建一個工程,由於如今默認的工程都開啓了自動的引用計數 ARC(Automatic Reference Count),咱們先修改工程設置,給 AppDelegate.m 加上 -fno-objc-arc
的編譯參數(以下圖所示),這個參數能夠啓用手工管理引用計數的模式。函數
而後,咱們在中輸入以下代碼,能夠經過 Log 看到相應的引用計數的變化。
- (BOOL)application:(UIApplication *)application |
運行結果:
Reference Count = 1
Reference Count = 2
Reference Count = 1 |
對 Linux 文件系統比較瞭解的同窗可能發現,引用計數的這種管理方式相似於文件系統裏面的硬連接。在 Linux 文件系統中,咱們用 ln
命令能夠建立一個硬連接(至關於咱們這裏的 retain),當刪除一個文件時(至關於咱們這裏的 release),系統調用會檢查文件的 link count 值,若是大於 1,則不會回收文件所佔用的磁盤區域。直到最後一次刪除前,系統發現 link count 值爲 1,則系統纔會執行直正的刪除操做,把文件所佔用的磁盤區域標記成未用。
從上面那個簡單的例子中,咱們還看不出來引用計數真正的用處。由於該對象的生命期只是在一個函數內,因此在真實的應用場景下,咱們在函數內使用一個臨時的對象,一般是不須要修改它的引用計數的,只須要在函數返回前將該對象銷燬便可。
引用計數真正派上用場的場景是在面向對象的程序設計架構中,用於對象之間傳遞和共享數據。咱們舉一個具體的例子:
假如對象 A 生成了一個對象 M,須要調用對象 B 的某一個方法,將對象 M 做爲參數傳遞過去。在沒有引用計數的狀況下,通常內存管理的原則是 「誰申請誰釋放」,那麼對象 A 就須要在對象 B 再也不須要對象 M 的時候,將對象 M 銷燬。但對象 B 可能只是臨時用一下對象 M,也可能以爲對象 M 很重要,將它設置成本身的一個成員變量,那這種狀況下,何時銷燬對象 M 就成了一個難題。
對於這種狀況,有一個暴力的作法,就是對象 A 在調用完對象 B 以後,立刻就銷燬參數對象 M,而後對象 B 須要將參數另外複製一份,生成另外一個對象 M2,而後本身管理對象 M2 的生命期。可是這種作法有一個很大的問題,就是它帶來了更多的內存申請、複製、釋放的工做。原本一個能夠複用的對象,由於不方便管理它的生命期,就簡單的把它銷燬,又從新構造一份同樣的,實在太影響性能。以下圖所示:
咱們另外還有一種辦法,就是對象 A 在構造完對象 M 以後,始終不銷燬對象 M,由對象 B 來完成對象 M 的銷燬工做。若是對象 B 須要長時間使用對象 M,它就不銷燬它,若是隻是臨時用一下,則能夠用完後立刻銷燬。這種作法看似很好地解決了對象複製的問題,可是它強烈依賴於 AB 兩個對象的配合,代碼維護者須要明確地記住這種編程約定。並且,因爲對象 M 的申請是在對象 A 中,釋放在對象 B 中,使得它的內存管理代碼分散在不一樣對象中,管理起來也很是費勁。若是這個時候狀況再複雜一些,例如對象 B 須要再向對象 C 傳遞對象 M,那麼這個對象在對象 C 中又不能讓對象 C 管理。因此這種方式帶來的複雜性更大,更不可取。
因此引用計數很好的解決了這個問題,在參數 M 的傳遞過程當中,哪些對象須要長時間使用這個對象,就把它的引用計數加 1,使用完了以後再把引用計數減 1。全部對象都遵照這個規則的話,對象的生命期管理就能夠徹底交給引用計數了。咱們也能夠很方便地享受到共享對象帶來的好處。
有些同窗想測試當對象釋放時,其 retainCount
是否變成了 0,他們的試驗代碼以下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions |
可是,若是你真的這麼實驗,你獲得的輸出結果多是如下這樣:
Reference Count = 1
Reference Count = 1 |
咱們注意到,最後一次輸出,引用計數並無變成 0。這是爲何呢?由於該對象的內存已經被回收,而咱們向一個已經被回收的對象發了一個 retainCount 消息,因此它的輸出結果應該是不肯定的,若是該對象所佔的內存被複用了,那麼就有可能形成程序異常崩潰。
那爲何在這個對象被回收以後,這個不肯定的值是 1 而不是 0 呢?這是由於當最後一次執行 release 時,系統知道立刻就要回收內存了,就沒有必要再將 retainCount 減 1 了,由於無論減不減 1,該對象都確定會被回收,而對象被回收後,它的全部的內存區域,包括 retainCount 值也變得沒有意義。不將這個值從 1 變成 0,能夠減小一次內存的寫操做,加速對象的回收。
拿咱們以前提到的 Linux 文件系統舉列,Linux 文件系統下刪除一個文件,也不是真正的將文件的磁盤區域進行抹除操做,而只是刪除該文件的索引節點號。這也和引用計數的內存回收方式相似,即回收時只作標記,並不抹除相關的數據。
ARC 可以解決 iOS 開發中 90% 的內存管理問題,可是另外還有 10% 內存管理,是須要開發者本身處理的,這主要就是與底層 Core Foundation 對象交互的那部分,底層的 Core Foundation 對象因爲不在 ARC 的管理下,因此須要本身維護這些對象的引用計數。
對於 ARC 盲目依賴的 iOS 新人們,因爲不知道引用計數,他們的問題主要體如今:
引用計數這種管理內存的方式雖然很簡單,可是有一個比較大的瑕疵,即它不能很好的解決循環引用問題。以下圖所示:對象 A 和對象 B,相互引用了對方做爲本身的成員變量,只有當本身銷燬時,纔會將成員變量的引用計數減 1。由於對象 A 的銷燬依賴於對象 B 銷燬,而對象 B 的銷燬與依賴於對象 A 的銷燬,這樣就形成了咱們稱之爲循環引用(Reference Cycle)的問題,這兩個對象即便在外界已經沒有任何指針可以訪問到它們了,它們也沒法被釋放。
不止兩對象存在循環引用問題,多個對象依次持有對方,形式一個環狀,也能夠形成循環引用問題,並且在真實編程環境中,環越大就越難被發現。下圖是 4 個對象造成的循環引用問題。
解決循環引用問題主要有兩個辦法,第一個辦法是我明確知道這裏會存在循環引用,在合理的位置主動斷開環中的一個引用,使得對象得以回收。以下圖所示:
主動斷開循環引用這種方式常見於各類與 block 相關的代碼邏輯中。例如在我開源的 YTKNetwork 網絡庫中,網絡請求的回調 block 是被持有的,可是若是這個 block 中又存在對於 View Controller 的引用,就很容易產生從循環引用,由於:
self
,因此持有了 Controller解決辦法就是,在網絡請求結束後,網絡請求對象執行完 block 以後,主動釋放對於 block 的持有,以便打破循環引用。相關的代碼見:
// https://github.com/yuantiku/YTKNetwork/blob/master/YTKNetwork/YTKBaseRequest.m
// 第 147 行:
- (void)clearCompletionBlock {
// 主動釋放掉對於 block 的引用
self.successCompletionBlock = nil;
self.failureCompletionBlock = nil;
} |
不過,主動斷開循環引用這種操做依賴於程序員本身手工顯式地控制,至關於回到了之前 「誰申請誰釋放」 的內存管理年代,它依賴於程序員本身有能力發現循環引用而且知道在什麼時機斷開循環引用回收內存(這一般與具體的業務邏輯相關),因此這種解決方法並不經常使用,更常見的辦法是使用弱引用 (weak reference) 的辦法。
弱引用雖然持有對象,可是並不增長引用計數,這樣就避免了循環引用的產生。在 iOS 開發中,弱引用一般在 delegate 模式中使用。舉個例子來講,兩個 ViewController A 和 B,ViewController A 須要彈出 ViewController B,讓用戶輸入一些內容,當用戶輸入完成後,ViewController B 須要將內容返回給 ViewController A。這個時候,View Controller 的 delegate 成員變量一般是一個弱引用,以免兩個 ViewController 相互引用對方形成循環引用問題,以下所示:
弱引用的實現原理是這樣,系統對於每個有弱引用的對象,都維護一個表來記錄它全部的弱引用的指針地址。這樣,當一個對象的引用計數爲 0 時,系統就經過這張表,找到全部的弱引用指針,繼而把它們都置成 nil。
從這個原理中,咱們能夠看出,弱引用的使用是有額外的開銷的。雖然這個開銷很小,可是若是一個地方咱們確定它不須要弱引用的特性,就不該該盲目使用弱引用。舉個例子,有人喜歡在手寫界面的時候,將全部界面元素都設置成 weak 的,這某種程度上與 Xcode 經過 Storyboard 拖拽生成的新變量是一致的。可是我我的認爲這樣作並不太合適。由於:
Xcode 的 Instruments 工具集能夠很方便的檢測循環引用。爲了測試效果,咱們在一個測試用的 ViewController 中填入如下代碼,該代碼中的 firstArray
和 secondArray
相互引用了對方,構成了循環引用。
- (void)viewDidLoad
{
[super viewDidLoad];
NSMutableArray *firstArray = [NSMutableArray array];
NSMutableArray *secondArray = [NSMutableArray array];
[firstArray addObject:secondArray];
[secondArray addObject:firstArray];
} |
在 Xcode 的菜單欄選擇:Product -> Profile,而後選擇 「Leaks」,再點擊右下角的」Profile」 按鈕開始檢測。以下圖
這個時候 iOS 模擬器會運行起來,咱們在模擬器裏進行一些界面的切換操做。稍等幾秒鐘,就能夠看到 Instruments 檢測到了咱們的此次循環引用。Instruments 中會用一條紅色的條來表示一次內存泄漏的產生。以下圖所示:
咱們能夠切換到 Leaks 這欄,點擊」Cycles & Roots」,就能夠看到以圖形方式顯示出來的循環引用。這樣咱們就能夠很是方便地找到循環引用的對象了。
下面咱們就來簡單介紹一下對底層 Core Foundation 對象的內存管理。底層的 Core Foundation 對象,在建立時大多以 XxxCreateWithXxx 這樣的方式建立,例如:
// 建立一個 CFStringRef 對象
CFStringRef str= CFStringCreateWithCString(kCFAllocatorDefault, 「hello world", kCFStringEncodingUTF8);
// 建立一個 CTFontRef 對象
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL); |
對於這些對象的引用計數的修改,要相應的使用 CFRetain
和 CFRelease
方法。以下所示:
// 建立一個 CTFontRef 對象
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
// 引用計數加 1
CFRetain(fontRef);
// 引用計數減 1
CFRelease(fontRef); |
對於 CFRetain
和 CFRelease
兩個方法,讀者能夠直觀地認爲,這與 Objective-C 對象的 retain
和 release
方法等價。
因此對於底層 Core Foundation 對象,咱們只須要延續之前手工管理引用計數的辦法便可。
除此以外,還有另一個問題須要解決。在 ARC 下,咱們有時須要將一個 Core Foundation 對象轉換成一個 Objective-C 對象,這個時候咱們須要告訴編譯器,轉換過程當中的引用計數須要作如何的調整。這就引入了bridge
相關的關鍵字,如下是這些關鍵字的說明:
__bridge
: 只作類型轉換,不修改相關對象的引用計數,原來的 Core Foundation 對象在不用時,須要調用 CFRelease 方法。__bridge_retained
:類型轉換後,將相關對象的引用計數加 1,原來的 Core Foundation 對象在不用時,須要調用 CFRelease 方法。__bridge_transfer
:類型轉換後,將該對象的引用計數交給 ARC 管理,Core Foundation 對象在不用時,再也不須要調用 CFRelease 方法。咱們根據具體的業務邏輯,合理使用上面的 3 種轉換關鍵字,就能夠解決 Core Foundation 對象與 Objective-C 對象相對轉換的問題了。
在 ARC 的幫助下,iOS 開發者的內存管理工做已經被大大減輕,可是咱們仍然須要理解引用計數這種內存管理方式的優勢和常見問題,特別要注意解決循環引用問題。對於循環引用問題有兩種主要的解決辦法,一是主動斷開循環引用,二是使用弱引用的方式避免循環引用。對於 Core Foundation 對象,因爲不在 ARC 管理之下,咱們仍然須要延續之前手工管理引用計數的辦法。
在調試內存問題時,Instruments 工具能夠很好地對咱們進行輔助,善用 Instruments 能夠節省咱們大量的調試時間。
原文連接:http://blog.devtang.com/2016/07/30/ios-memory-management/