iOS的記憶體管理

來源:互聯網
上載者:User

標籤:面向   刪除   mutable   view   菜單   額外   讀者   功能表列   檔案   

最近看了兩本書,《Objective-C 進階編程,iOS與OS X多線程和記憶體管理》,《Effective Object-C2.0》。iOS開發必看的兩本書,非常推薦。但是看記憶體管理的時候有一句話非常不理解:自己產生並持有對象,引用計數為1。後面還有一句:對象alloc的時候預設引用計數為1。自己建立的時候,如果預設是1,那麼自己再持有他,要再加1,這時候就是2了。但是實際結果是1。查看了很多資料,我的理解是:建立對象的時候預設為0,引用計數,顧名思義就是引用該對象的計數。強引用一次,引用計數+1。不再引用該對象,則引用計數-1。出了範圍,引用計數-1。以上是對記憶體管理的簡單理解,詳細內容去看書。還有一篇唐巧的文章,寫的非常好,轉來給大家看:

理解 iOS 的記憶體管理

遠古時代的故事

那些經曆過手工管理記憶體(MRC)時代的人們,一定對 iOS 開發中的記憶體管理記憶猶新。那個時候大約是 2010 年,國內 iOS 開發剛剛興起,tinyfool 大叔的大名已經如雷貫耳,而我還是一個默默無聞的剛畢業的小子。那個時候的 iOS 開發過程是這樣的:

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

是的,這就是那個年代的 iOS 開發人員,通常情況下,我們在開發完一個功能後,需要再花好幾個小時,才能把引用計數管理好。

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

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

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)等語言也提供了基於引用計數的記憶體管理方式。

為了更形象一些,我們再來看一段 Objective-C 的代碼。建立一個工程,因為現在預設的工程都開啟了自動的引用計數 ARC(Automatic Reference Count),我們先修改工程設定,給 AppDelegate.m 加上 -fno-objc-arc 的編譯參數(如所示),這個參數可以啟用手工管理引用計數的模式。

然後,我們在中輸入如下代碼,可以通過 Log 看到相應的引用計數的變化。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{NSObject *object = [[NSObject alloc] init];NSLog(@"Reference Count = %u", [object retainCount]);NSObject *another = [object retain];NSLog(@"Reference Count = %u", [object retainCount]);[another release];NSLog(@"Reference Count = %u", [object retainCount]);[object release];// 到這裡時,object 的記憶體被釋放了return YES;}

運行結果:

Reference Count = 1Reference Count = 2Reference 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{NSObject *object = [[NSObject alloc] init];NSLog(@"Reference Count = %u", [object retainCount]);[object release];NSLog(@"Reference Count = %u", [object retainCount]);return YES;}

但是,如果你真的這麼實驗,你得到的輸出結果可能是以下這樣:

Reference Count = 1Reference Count = 1

我們注意到,最後一次輸出,引用計數並沒有變成 0。這是為什麼呢?因為該對象的記憶體已經被回收,而我們向一個已經被回收的對象發了一個 retainCount 訊息,所以它的輸出結果應該是不確定的,如果該對象所佔的記憶體被複用了,那麼就有可能造成程式異常崩潰。

那為什麼在這個對象被回收之後,這個不確定的值是 1 而不是 0 呢?這是因為當最後一次執行 release 時,系統知道馬上就要回收記憶體了,就沒有必要再將 retainCount 減 1 了,因為不管減不減 1,該對象都肯定會被回收,而對象被回收後,它的所有的記憶體地區,包括 retainCount 值也變得沒有意義。不將這個值從 1 變成 0,可以減少一次記憶體的寫操作,加速對象的回收。

拿我們之前提到的 Linux 檔案系統舉列,Linux 檔案系統下刪除一個檔案,也不是真正的將檔案的磁碟地區進行抹除操作,而只是刪除該檔案的索引節點號。這也和引用計數的記憶體回收方式類似,即回收時只做標記,並不抹除相關的資料。

ARC 下的記憶體管理問題

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

對於 ARC 盲目依賴的 iOS 新人們,由於不知道引用計數,他們的問題主要體現在:

  1. 過度使用 block 之後,無法解決循環參考問題。
  2. 遇到底層 Core Foundation 對象,需要自己手工管理它們的引用計數時,顯得一籌莫展。
循環參考(Reference Cycle)問題

引用計數這種管理記憶體的方式雖然很簡單,但是有一個比較大的瑕疵,即它不能很好的解決循環參考問題。如所示:對象 A 和對象 B,相互引用了對方作為自己的成員變數,只有當自己銷毀時,才會將成員變數的引用計數減 1。因為對象 A 的銷毀依賴於對象 B 銷毀,而對象 B 的銷毀與依賴於對象 A 的銷毀,這樣就造成了我們稱之為循環參考(Reference Cycle)的問題,這兩個對象即使在外界已經沒有任何指標能夠訪問到它們了,它們也無法被釋放。

不止兩對象存在循環參考問題,多個對象依次持有對方,形式一個環狀,也可以造成循環參考問題,而且在真實編程環境中,環越大就越難被發現。是 4 個對象形成的循環參考問題。

主動斷開循環參考

解決循環參考問題主要有兩個辦法,第一個辦法是我明確知道這裡會存在循環參考,在合理的位置主動斷開環中的一個引用,使得對象得以回收。如所示:

主動斷開循環參考這種方式常見於各種與 block 相關的代碼邏輯中。例如在我開源的 YTKNetwork 網路程式庫中,網路請求的回調 block 是被持有的,但是如果這個 block 中又存在對於 View Controller 的引用,就很容易產生從循環參考,因為:

  • Controller 持有了網路請求對象
  • 網路請求對象持有了回調的 block
  • 回調的 block 裡面使用了 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 拖拽產生的新變數是一致的。但是我個人認為這樣做並不太合適。因為:

  1. 我們在建立這個對象時,需要注意臨時使用一個強引用持有它,否則因為 weak 變數並不持有對象,就會造成一個對象剛被建立就銷毀掉。
  2. 大部分 ViewController 的視圖對象的生命週期與 ViewController 本身是一致的,沒有必要額外做這個事情。
  3. 早先蘋果這麼設計,是有曆史原因的。在早年,當時系統收到 Memory Warning 的時候,ViewController 的 View 會被 unLoad 掉。這個時候,使用 weak 的視圖變數是有用的,可以保持這些記憶體被回收。但是這個設計已經被廢棄了,替代方案是將相關視圖的 CALayer 對應的 CABackingStore 類型的記憶體區會被標記成 volatile 類型,詳見《再見,viewDidUnload方法》。
使用 Xcode 檢測循環參考

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 對象的記憶體管理。底層的 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); // 引用計數加 1CFRetain(fontRef);// 引用計數減 1CFRelease(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 可以節省我們大量的調試時間。

願每一個 iOS 開發人員都可以掌握 iOS 的記憶體管理技能。

iOS的記憶體管理

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.