天天看點

《iOS應用開發》——2.3節記憶體管理

本節書摘來自異步社群《ios應用開發》一書中的第2章,第2.3節記憶體管理,作者【美】richard warren,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

2.3 記憶體管理

ios應用開發

我不是吓唬你們。在ios 5.0系統之前,記憶體管理毫無疑問是ios開發最困難的部分。簡而言之,問題是這樣的。無論何時你建立了一個變量,你就要在記憶體中給它配置設定一定的空間。對于局部變量來說,我們通常使用棧上的記憶體,這些記憶體是自動管理的,當函數傳回時,函數中定義的任何局部變量都會從記憶體中自動删除。

這聽起來很棒,但是棧有兩個嚴重的局限。首先,它的空間非常有限,如果用盡了記憶體,應用程式就會崩潰。其次,這些變量很難共享。請記住,函數使用值傳參和傳回。這意味着所有傳入函數或者從函數傳出的内容都是複制值。如果你僅僅處理整型和浮點資料,這不會有問題。但是如果開始處理大型、複雜的資料結構時會發生什麼呢?從一個函數傳遞一個參數到另一個函數,并且你會發現自己在複制一個副本的副本。這會迅速浪費很多時間和記憶體。

還有另外一個方法,我們可以在堆中聲明變量并且使用指針來通路其記憶體空間。這有幾個優點。在堆上我們有更多的可用記憶體空間,并且我們可以自由地傳遞指針,隻有指針本身被複制了,而不是整個資料結構。

然而,當我們使用堆時,我們必須手動管理它的記憶體。當我們需要一個新的變量時,我們必須在堆上請求足夠的記憶體空間。接着,當我們使用完畢時,必須釋放這個空間,讓它能夠被重複使用。這會導緻兩個常見的記憶體相關的故障。

首先,你釋放了記憶體但是卻不小心繼續使用了它。這就叫做野指針。這一類型的故障很難查找出來。釋放一個變量并不需要改變儲存在堆中的實際值。它隻是告訴os:這塊記憶體可以再次被使用了。貌似被釋放記憶體的指針還可以繼續正常使用,隻會在系統最終重新使用這塊記憶體,并且覆寫了原來的值的時候才會使你遇到麻煩。

這會在完全不相關部分的代碼産生很奇怪的錯誤。想象這種情況:你在堆中釋放了a對象的記憶體但是不小心地繼續在使用它的指針。同時,一個完全不相關部分的代碼在堆上請求了一個新的對象b。它被配置設定了一塊記憶體,部分地覆寫了對象a。現在你改變對象a的一個值,這在對象b中的某個部分儲存了新的資料,讓b變成了無效的狀态。下一次你想要從對象b讀取值的時候,就會出錯(或者有可能得到非常詭異的結果)。

第二個常見的記憶體錯誤是忘記釋放記憶體,這稱為記憶體洩漏,并且這會導緻應用程式在運作過程中占用越來越多的記憶體。

不過,情況并沒有那麼糟糕。當應用程式退出時所有的記憶體都會被釋放。我曾經聽說過有的伺服器軟體故意洩露它的所有記憶體。畢竟,如果你同時運作成千上萬的伺服器副本,周期性地殺死和啟動新的副本比冒着因野指針導緻程式崩潰要容易處理得多。

然而,在ios系統中,我們不能那麼奢侈地使用記憶體。所有的ios裝置都有嚴格的記憶體限制。使用過多記憶體,應用就會被系統殺死。是以正确和有效的記憶體管理是一個至關重要的技能。

對我們來說,我們主要關注的是對象。所有的對象都是在堆中建立的。是的,你也可以在堆上建立c資料類型變量或者結構,但是這是非常罕見的(可以找一本關于c語言程式設計的好書看下具體的細節)。大部分情況下,ios記憶體管理就是管理我們對象的生命周期。

2.3.1 對象和保留計數

最大的問題之一僅僅是确定代碼的哪個部分應該負責對象的記憶體釋放。在簡單的例子中,解決方案總是顯得微不足道。然而,一旦你開始傳遞對象,将它們儲存在其他對象的執行個體變量中或将它們放置在集合中,很快就開始陷入困境了。

要解決這個問題,objective-c程式習慣使用引用計數。當我們建立一個對象的時候,引用計數從1開始。我們可以通過在對象中調用retain方法來增加引用計數。類似地,我們可以通過調用release方法來減少引用計數。當引用計數等于0時,系統從記憶體上删除這個對象。

這讓傳遞對象成為可能,并且不用擔心歸屬問題。如果你想要持有一個對象,你就保留它。當你完成使用,你就釋放它。隻要你正确地使用所有的保留和釋放方法(更不用說自動釋放和自動釋放池了),你就既能夠避免野指針也能夠避免洩露記憶體。

當然,讓一切運轉正常并不像看上去的那麼容易。objective-c的記憶體管理規定有很多規則和奇怪的情況。此外,作為開發者,我們必須總是保證100%的正确性。是以每當我們建立一個新的變量,經常會機械地遵循以下的步驟。這是非常令人生厭的。它建立了很多機械的代碼,其中很多嚴格意義上來說并不是必需的,但是如果我們不是每次都嚴格遵守每個步驟,就會有忘記某些真正重要的東西的風險。

當然,蘋果公司也試圖提供幫助。instruments有一系列追蹤配置設定和釋放、查找記憶體洩漏的工具。在近期的xcode版本中,靜态分析器在分析代碼和查找記憶體管理錯誤方面已經做得越來越好了。

這也提出一個問題:如果靜态分析器可以找到這些錯誤,為什麼不能直接修複它們呢?畢竟記憶體管理是一件枯燥、針對細節的任務,需要遵循一套嚴格的規則,正是編譯器所擅長的任務。于是就有了一種非常新的管理記憶體的技術:自動引用計數(arc)。

2.3.2 介紹arc

arc是一個為objective-c提供自動記憶體管理的編譯器特征。理論上來說,arc遵循了保留和釋放記憶體管理的慣例(檢視蘋果公司的memory management programming guide以獲得更多的資訊)。當你的程式在編譯時,arc分析代碼并且自動地添加必需的retain,release和autorelease方法調用。

對于開發者來說,這是非常好的消息。我們不再需要擔心自己管理記憶體。取而代之的是,我們可以将時間和注意力集中在應用程式真正有意思的部分,比如實作新的特征,簡化使用者界面,或者提高穩定性和性能等等。

此外,蘋果公司已經在arc模式下提高了記憶體管理的性能。例如,arc的自動保留和釋放調用比同等的手動記憶體管理要快了2.5倍。新的@autoreleasepool程式塊比老的nsautoreleasepool對象要快了6倍,并且objec<code>_</code>m<code>sgsend()甚至都要比原先快了33%。最後這一點是尤其重要的,因為objec</code>_``nsgsend()是用來排程應用中每個objective-c方法的調用。

總體來說,arc讓objective-c更容易學,生産效率更高,更易于維護,更加安全、更加穩定并且更加快速。這就是我所說的多赢局面。大部分情況下,我們甚至不需要考慮記憶體管理。我們僅僅需要寫代碼,建立和使用對象就可以了。arc會為我們處理所有複雜的細節。

比較arc和垃圾收集

arc不是垃圾收集。它們有同一個目标,兩者都是自動記憶體管理技術,讓應用開發變得更加簡單。然而,它們用了不同的方法。垃圾收集在運作時追蹤對象。當它确定對象不再使用時,它就會從記憶體中删除對象。

不妙的是,這産生了幾個潛在的性能問題。基礎代碼需要監視并且删除對象,這便在我們應用中加入了額外的負擔。我們同樣也幾乎控制不了垃圾收集器啟動掃描。雖然現代的垃圾收集器嘗試減少它們對應用程式性能産生的影響,但是它們天生就是無法确定的。這意味着垃圾收集器會導緻你的應用程式變慢或者在應用程式執行過程中會随時暫停。

而arc在編譯的時候就完成了所有的記憶體管理。在運作的時候就沒有額外的負擔,事實上,由于大量的優化算法,arc代碼比手動記憶體管理運作地更快。此外,記憶體管理系統是完全确定的,這就意味着不會存在無法預料的事情發生。

找到并且消除循環保留

雖然arc代表了記憶體管理的巨大進步,但是它沒有完全讓開發者們不用思考有關記憶體的問題。在arc的模式下仍然有可能造成記憶體洩漏,arc仍然容易受保留循環的影響。

為了弄清楚這個問題,我們需要窺視隐藏在背後的東西。預設情況下,所有arc中的變量都是采用強引用。當你把一個對象賦給強引用時,系統會自動保留該對象。當你從引用中删除中這個對象時(通過指派一個新的對象給這個變量或者通過指派nil給這個變量),系統就會釋放這個對象。

當兩個對象通過強引用直接或間接互相引用對方時,就會産生保留循環。這通常在父子層級中會發生,當子對象持有父對象的引用時。

想象一下我有一個person對象,定義如下:

在這個例子中,當我們調用mymethod的時候,兩個person對象就建立了。每個都以保留計數1開始。接着我們将child對象指派給parent的child功能。這使child的保留計數增加到2。

arc自動地在方法的末尾插入釋放的調用。這讓parent的保留計數降為0,child的保留計數降為1。由于parent的保留計數現在等于0,它就被删除了。接着,arc自動釋放了所有parent的屬性。是以,child對象的保留計數也降為0并且它也被删除了,所有的記憶體都被釋放了,就像我們所期望的一樣。

現在,讓我們添加一個保留循環。如下所示改變mymethod:

和之前一樣,我們建立了parent和child對象,每個對象的保留計數都是1。這次,parent獲得了child的一個引用,child也獲得了parent的一個引用,這讓雙方的引用計數都增加到2。在程式的末尾,arc自動釋放了對象,讓兩個對象的引用計數都減少到1。由于兩個對象的引用計數都不是0,是以兩個對象都沒有被删除,是以我們的保留循環就造成了記憶體洩漏。

幸運的是,arc有一個解決辦法。我們隻需要重新定義parent屬性,讓它使用置零弱引用(zeroing weak reference)。本質上,這意味着将屬性的特性從strong改為weak。

置零弱引用有兩個優點。首先,它們不會增加對象的保留計數,是以它們也不會延長對象的生命周期。其次,當它們指向的對象删除後,系統就會自動将它們設為nil。這也避免了野指針的産生。

應用一般都會有從一個根對象延伸出對象層級圖。當引用圖的上層對象(任何更靠近圖的根對象)時,我們通常會使用置零弱引用。此外,對于所有委托和資料源,我們應該也使用置零弱引用(請看本章後面的“委托”一節)。這是objective-c的一個标準慣用法,因為它有助于避免由于粗心大意造成的保留循環。

目前為止,我們所見過的循環都是很明顯的,但是并不總是這樣。保留循環可以包含任何多的對象,隻要它最終指向自身就行。一旦你超過了三或四層的引用,追蹤可能的循環幾乎就變得不可能了。幸運的是,開發工具又一次拯救了我們。

在xcode 4.2中,instruments目前有搜尋保留循環的能力,并且會圖形化呈現出保留循環。

隻要分析一下你的應用程式。在主菜單中選擇product &gt; profile。在instrument的彈出視窗中選擇leaks模闆并點選profile(見圖2.3)。這樣會同時啟動instruments和應用程式。

instruments将會以兩個診斷工具開始。allocations會追蹤記憶體配置設定和釋放,leaks會查找洩漏的記憶體。leaks将會在leaks instrument中以紅色條塊的形式顯示。選擇leaks instrument行,詳情就會顯示在下面。在跳轉工具條中,把細節視圖從leaks改為cycles,然後instruments就會顯示保留循環(見圖2.4)。現在就可以輕按兩下這個讨厭的引用,xcode會自動定位它。修複它(通過将其轉變為置零弱引用,或者通過重構應用程式),并且再次測試。我們将會在補充章節b節中詳細讨論instruments(www.freelancemadscience.com/book)。

《iOS應用開發》——2.3節記憶體管理

使用規則

arc使用起來難以置信地容易。大部分時間裡,我們不需要做任何事。xcode 4.2的所有項目模闆都預設使用arc,雖然我們如果需要的話可以在某個檔案上啟用手動記憶體管理。這讓我們自由地将arc和非arc代碼結合在一起,并且arc可以在使用ios 4.0及以上版本的項目中使用(會有一些限制)。xcode甚至還有工具可以轉換非arc引用的應用程式(選擇edit &gt; refactor &gt; convert to objective-c arc)。

為了讓arc正确地運轉,編譯器必須正确地了解我們的意圖。這就需要我們遵守一些額外的規則來幫助消除所有的歧義性。如果你不了解這些規則的有關内容(這樣的情況在日常的程式設計中極少發生),不用擔心。最重要的是,編譯器會執行所有的這些規則,打破它們就會産生錯誤。這就讓問題很容易找出來并修複。

所有的規則如下列所示。

不要在c結構體中使用對象指針。而是建立一個objective-c的類來存儲這些資料。

不要建立名稱以“new”開頭的屬性。

不要調用dealloc、retain、release、retaincount或者autorelease方法。

不要實作自己的retain、release、retaincount或者autorelease方法。你可以實作dealloc方法,但是通常沒有必要。

在arc中實作dealloc方法時,不要調用[<code>super</code>dealloc``]。編譯器會自動調用。

不要使用nsautoreleasepool對象,而是使用新的@autoreleasepool``{}程式塊。

一般來說,不要在id和void*之間做類型轉換。當在objective-c對象和core foundation類型之間移動資料,應該使用增強的類型轉換或者宏指令(參考下一節)。

不要使用nsallocateobject或者nsdeallocateobject。

不要使用記憶體區(memory zones)或者nszone。

arc和自由橋接(toll-free bridging)

首先,要明白arc隻能應用于objective-c對象,這是非常重要的。如果在堆上給任何c資料結構配置設定記憶體,你還是需要自己管理記憶體。

當使用core foundation資料類型時,真正的問題才會發生。core foundation是一個低級的c api,提供了許多基本的資料管理和os服務。有個類似的objective-c架構也命名為foundation(你困惑了嗎?)。不必驚訝,foundation和core foundation是有着緊密的聯系的。事實上,foundation為很多core foundation服務和資料類型提供了objective-c 接口。

現在讓我們來看一些很酷的東西。很多foundation和core foundation資料類型都是可以互相轉換的。本質上,nsstring和cfstring是完全相等的。我們可以随意地将cfstring的引用傳遞給接受nsstring類型參數的objective-c方法,或者将nnstring引用傳遞給接受cfstring類型參數的core foundation中的函數。這種互操作性稱為自由橋接(參見蘋果公司的cocoa fundamentals guide &gt; cocoa objects &gt; toll-free bridging以便檢視更多資訊)。

不妙的是,當來來回回進行類型轉換,記憶體管理就變得混亂了。哪個資料由誰來管理?我們要使用cfretain()和cfrelease()來手動管理記憶體嗎,還是就讓arc自己完成?

有三個基本情況我們需要注意:由objective-c方法傳回的core foundation資料;使用core foundation記憶體管理函數的代碼;沒有使用任何記憶體管理函數把資料傳入core foundation或者從其接收資料的代碼。

第一個情況比較簡單。如果我們能夠通過調用objective-c方法通路core foundation對象,我們就不需要做任何事了。我們可以随意把資料做類型轉換,并且編譯器也不會産生任何錯誤。

這樣是能運作的,因為所有的objective-c方法都遵循objective-c的記憶體管理約定(參見蘋果公司的memory management programming guide以便檢視更多資訊)。arc能了解這些規定,正确地解釋出資料的歸屬,并且進行正确的操作。

在這個例子中,我們調用了uiimage的cgimage屬性。它傳回了一個cgimageref。然後我們把這個c資料類型轉換成objective-c的id,并且将其放在一個數組中。因為我們用objective-c方法來通路c資料,是以編譯器允許簡單的類型轉換,然後剩下的事情由arc來處理。

第二個情況中,如果我們調用任何core foundation記憶體管理函數,我們需要讓arc知道。這些函數包括cfretain``()、cfrelease()和任何在名稱中含有create和copy的函數(參見蘋果公司的memory management programming guide關于core foundation的内容擷取更多的資訊)。

如果core foundation函數保留了資料(cfretain、create或者c<code>opy),我們需要使用_</code>bridge<code>_</code>transfer标記做類型轉換。如果core foundation釋放了這些資料(cfrelease),然後我們就需要_<code>bridge</code>_<code></code>retain标記做類型轉換。下面是這兩個操作的示例:

花一些時間來熟悉xcode的參考文檔是非常值得的。初學objective-c時,這是一個非常好的資源,并且它在你的ios開發職業生涯中,會一直起着不可或缺的參考指南的作用。

讓我們來做一個小練習。打開參考文檔并且查找uiwindow,接着從結果中選擇uiwindow class reference。類參考分為好幾個部分。它以類的簡單描述開始,包括它所繼承的類層級,它所實作的協定,以及一些使用該類的示例代碼。

下面是關于類的概覽。這個部分描述了它的基本用途,包括所有重要的實作和使用細節。

接着就可以看到任務清單了。它将類的特性和方法按照它們的預期用途分組(對于uiwindow來說,這些分組就是configuring windows、making windows key、converting coordinates和sending events)。清單上的每一項都有一個超級連結,連結到文檔後面部分更加詳細的描述資訊。

然後就是描述類特性、類方法、常量和通知的部分了。(不是所有的類都有這些類别。例如uiwindow就沒有類方法。)每個部分都包含了該類定義的所有相關的公有條目。如果想要看父類中所聲明的條目(或者是類層級中更高的類),你就需要查找相應的類參考文檔。

每項的詳細條目呈現了該項是如何聲明的,描述了它的用途,并且提供了可用性相關的資訊。例如,通過uiwindow的rootviewcontroller屬性可以通路管理該視窗内容的uiviewcontroller。它的預設值是nil,并且它隻有在ios 4.0和更高的版本中才可用。

相比之下,cgpoint和uiwindow類不一樣,cgpoint沒有自己單獨的參考。它是在cggeometry參考中被描述的。和類參考一樣,這個參考也是以簡短的描述和概覽開始。接着它列出了按照任務分組的幾何函數。最後參考文檔列出了所有相關的資料類型和常量。

cgpoint的條目包含了結構體的聲明和它所有的域和可用性的描述。

當我們在學習這本書時,會定期地抽時間看看參考文檔中的新類和結構。我會嘗試展示一些類的典型用途的好例子,但是大部分類都包含太多的方法和特征了,不可能詳細地講解每個細節。此外,經常溫習參考文檔将會拓寬和啟迪你的思路,有助于你在開發自己的應用程式時找到更好的方法。

繼續閱讀