iOS內存管理策略和實踐

 
內存管理策略(memory Management Policy)
NSObject protocol中定義的的方法和標準命名慣例一起提供了一個引用計數環境,內存管理的基本模式處於這個環境中。NSObject類定義了一個方法叫dealloc,當對象銷燬的時候,dealloc會被自動調用。本文描述,在Cocoa中所有正確管理內存基本規則,並提供了一些使用正確的例子。
 
【基本的內存管理規則】
內存管理模式基於對象的“所有權”上。任何對象都會被有一個或多個使用者引用,只要對象還有一個使用者,該對象就應該繼續存在。如果一個對象沒有使用者了,系統將自動銷燬它。爲了讓開發者清晰的瞭解:使用對象和不再使用對象的場景,Cocoa設置了以下策略:
1.管好自己創建的對象。開發者使用alloc、new、copy和mutableCopy來創建對象。
 
2.使用retain來獲得對象的所有權。某個函數接受的對象,通常保證在該函數調用期間仍然可用,並可以安全返回對象給上層調用者。開發者在以下兩種情況下使用retain
1)在“訪問函數”(accessor)的實現中或者在init方法,爲了將對象作爲自己的屬性。
2)防止對象被其他操作釋放掉,從而變爲無效的對象。
 
3.當你不在需要的時候,必須放棄對象所有權。
 
一個簡單的例子
看看下面的代碼段,可以證明剛剛的所說的策略
  1. {   
  2.     Person *aPerson = [[Person alloc] init];   
  3.     // ...   
  4.     NSString *name = aPerson.fullName;   
  5.     // ...   
  6.     [aPerson release];   
  7. }   
Person被通過alloc創建之後,當Person不在使用的時候,發送了一個release的消息。name這個變量沒有使用,所以name不必發送release消息。
 
使用autorelease來發送一個延遲的release
典型的使用場景:函數返回一個對象的時候。例如,你可以像這樣實現fullName的方法:
  1. - (NSString *)fullName {   
  2.     NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@",   
  3.                                           self.firstName, self.lastName] autorelease];   
  4.     return string;   
  5. }   
上面就是典型的場景:你想放棄對象的所有權,但是又想讓調用者在string銷燬前使用返回值。
 
還可以通過下面的實現達到上面的效果:
  1. - (NSString *)fullName {   
  2.     NSString *string = [NSString stringWithFormat:@"%@ %@",   
  3.                                  self.firstName, self.lastName];   
  4.     return string;   
  5. }   
根據命名慣例,full name方法不具備返回值的所有權。因此,調用者無需對返回值string進行release
 
開發者不應該獲得“通過引用傳遞的對象”的所有權
Cocoa中的一些方法,指定是傳遞引用。例如NSError對象包涵錯誤的信息,比如:initWithContentsOfURL:options:error: (NSData) and initWithContentsOfFile:encoding:error: (NSString).這種情況,之前的規則中已經描述過了。你調用這些方法,但是沒有創建NSError對象,所以,你沒有它的所有權。因此不用release,比如:
  1. NSString *fileName = <#Get a file name#>;   
  2. NSError *error;   
  3. NSString *string = [[NSString alloc] initWithContentsOfFile:fileName   
  4.                         encoding:NSUTF8StringEncoding error:&error];   
  5. if (string == nil) {   
  6.     // Deal with error...   
  7. }   
  8. // ...   
  9. [string release];   
 
【實現dealloc放棄對象的所有權】
NSObject類定義了一個方法dealloc,當某個對象沒有使用者,並它的內存是可再生的,delloc就自動被調用。delloc的角色就是釋放對象佔用的內存並且處理自己所擁有的資源,包括本身變量的釋放。
下面的代碼展示了,如何實現Person類的dealloc函數。
  1. @interface Person : NSObject   
  2. @property (retain) NSString *firstName;   
  3. @property (retain) NSString *lastName;   
  4. @property (assign, readonly) NSString *fullName;   
  5. @end   
  6.     
  7. @implementation Person   
  8. // ...   
  9. - (void)dealloc   
  10.     [_firstName release];   
  11.     [_lastName release];   
  12.     [super dealloc];   
  13. }   
  14. @end   
 
【重要】
任何時候,不要直接調用某一對象的dealloc。
不許在dealloc的最後一行調用父類的dealloc
不要嘗試管理系統資源。(參考內存管理實踐)
應用程序終止的時候,對象的dealloc可能不會被調用。因爲進程的內存是自動清除退出,讓操作系統清理資源比調用所有的內存管理方法更有效地。
 
Core Foundation使用相似的卻又不同的規則
Core Foundation對象使用類似的內存管理規則(查看Memory Management Programming Guide for Core Foundation),但是Cocoa和Core Foundation的命名管理並不相同。尤其是,Core Foundation的Create Rule(在Memory Management Programming Guide for Core Foundation中查看“The Create Rule” )並不適用於返回Objective-C對象的方法。比如以下代碼片段:
  1. MyClass *myInstance = [MyClass createInstance]; 
 
 

 內存管理實踐

儘管基本的概念在“內存管理策略”文章中簡單得闡述了,但是還有一些實用的步驟讓你更容易管理內存;有助於確保你的程序最大限度地減少資源需求的同時,保持可靠和強大。

 

使用“訪問器方法”讓內存管理更簡單

假如,你的程序有一個對象類型的屬性,你必須保證:當你使用的時候,任何的已經賦值了的對象不會被銷燬。被賦新值的時候,開發者必須獲得對象的所有權,並放棄正在使用對象的所有權。

 

有時候,這些聽起來很老套和繁瑣,如果開發者統一使用訪問器方法,內存管理有問題的機會大大減少。如果開發者在代碼中總是使用retain和release管理實例變量,幾乎肯定會做錯事,換句話說:訪問器是必須的。

 

來看一個Counter對象

  1. @interface Counter : NSObject   
  2. @property (nonatomic, retain) NSNumber *count;   
  3. @end;   

該屬性聲明瞭兩個訪問器。典型的做法,開發者告訴編譯器合成(synthesize)訪問器方法。瞭解訪問器是如何實現的對開發者是有好處的。

 

在get訪問器中,開發者只需要返回變量即可,不需要retain和release

  1. - (NSNumber *)count {   
  2.     return _count;   
  3. }   

 

在set訪問器方法裏,如果每一位開發者都能按照相同的規則,對新的count進行負責,開發者必須通過retain來獲得對象的所有權。開發者也需要通過release來放棄舊對象的所有權。(在Objective-c中給對象發送nil消息是允許的,即是_count沒有被設置,仍然是安全的。)爲了防止兩個對象是相同的,開發者需要先調用[newCount retain](開發者可不希望意外的把對象給銷燬)

  1. - (void)setCount:(NSNumber *)newCount {   
  2.     [newCount retain];// retain new object first;   
  3.     [_count release];   
  4.     // Make the new assignment.   
  5.     _count = newCount;   
  6. }   

 

【使用訪問器方法設置屬性值】

假設你想實現一個重置counter的方法。你有多個選擇。

 

第一種方式:用alloc創建NSNumber實例,然後可以用release釋放。

  1. - (void)reset {   
  2.     NSNumber *zero = [[NSNumber alloc] initWithInteger:0];   
  3.     [self setCount:zero];   
  4.     [zero release];   
  5. }   

第二種方法:用便捷構造方法創建NSNumber對象。因此,不用調用retain和release等。

  1. - (void)reset {   
  2.     NSNumber *zero = [NSNumber numberWithInteger:0];   
  3.     [self setCount:zero];   
  4. }   

注意:以上兩種方法都使用訪問器方法。

 

下面將幾乎可以肯定,正常的情況下,因爲它可能避開訪問器方法,這樣很誘人。這樣做幾乎肯定會導致一個錯誤在某些時候。(比如,當開發者忘記retain或者release,或者將來內存管理機制有變化)

  1. - (void)reset {   
  2.     NSNumber *zero = [[NSNumber alloc] initWithInteger:0];   
  3.     [_count release];   
  4.     _count = zero;   
  5. }   

 注意:如果開發者使用key-value observing,上面這種方法也不屬於KVO範疇

 

不要在初始化和dealloc函數中使用訪問器方法

唯一不讓使用訪問器的地方就是initializer和dealloc。爲了將counter初始化爲零,開發者可以這樣實現:

  1. - init {   
  2.     self = [super init];   
  3.     if (self) {   
  4.         _count = [[NSNumber alloc] initWithInteger:0];   
  5.     }   
  6.     return self;   
  7. }   

 

爲了初始化爲非零數據,可以這麼實現initWithCount方法:

  1. - initWithCount:(NSNumber *)startingCount {   
  2.     self = [super init];   
  3.     if (self) {   
  4.         _count = [startingCount copy];   
  5.     }   
  6.     return self;   
  7. }   

 

既然Counter類有一個類屬性,開發者還需要實現dealloc,dealloc通過發送release,將放棄左右對象的所有權,最終dealloc還會調用父類的dealloc函數。

  1. - (void)dealloc {   
  2.     [_count release];   
  3.     [super dealloc];   
  4. }   

 

使用弱引用(Weak References)來避免循環retain

對一個對象retain操作是強引用(strong reference)。所有強引用都被release之後對象才被銷燬。如果,兩個對象有彼此的強引用,就出現衆所周知的問題——循環retain。(包括:直接,或通過其他對象強引用鏈的情況)

 

對象的關係如圖所示,有一個潛在的循環retain。Document對象每一頁都有一個Page對象。每個Page對象有一個paragraphs屬性,表明該Page在那一Document中。如果Document中的Page是強引用,Page類中的paragraphs屬性也是強引用,那個對象都不會被銷燬。Document的引用值直到Page對象被釋放才變爲0,而Page對象直到Document釋放才被釋放。

解決循環引用的的方法是使用弱引用。弱引用是一種非佔有所有權的關係,不對源對象retain,只是引用(reference)。然後,爲了保持對象圖的完整性,強引用還是必要的(如果只有弱引用,pages和paragraphs將沒有任何的所有者,也就不能被釋放)Cocoa形成了一個慣例:父對象應該強引用子對象,子變量應該弱引用父對象。所以,在圖1中,Document對象強引用page對象,page對象弱引用Document對象。

 

Cocoa中的弱引用例子包含了(但不限於)table data sources, outline view items, notification observers, and miscellaneous targets and delegates.

 

開發者給弱引用對象發送消息應小心一些。如果給一個已經銷燬的對象發消息,程序將crash。當對象可用的時候,開發者需具備良好的定義條件(You must have well-defined conditions for when the object is valid.)。

 

大多數情況:弱引用對象知道其他對象弱引用了自己,當自己被銷燬的時候,有責任通知其他對象。比如,當開發者用notification center註冊一個對象,notification center存儲一個弱引用對象,併發送post消息給對象。當對象已經被銷燬了。

 

開發者需從notification center中註銷對象,防止notification center發送消息給不存在的對象。同樣,當一個delegate對象被銷燬後,開發者噓移除delegate通過發送一個參數爲nil的setDelegate消息而這些消息通常從對象的dealloc中發送。

 

避免銷燬正在使用的對象

Cocoa's所有權策略指定接受的對象應該,保證在函數調用範圍可用;還可以返回接受的對象,而不用擔心被release掉。保證從程序中的gerrer方法中返回實例變量或者計算後的值是沒問題的。

 

問題是,當需要的時候對象仍然有效。偶爾有些例外,主要是下面兩種情況:

 

1.從基礎集合類中移除對象

  1. heisenObject = [array objectAtIndex:n];   
  2. [array removeObjectAtIndex:n];   
  3. // heisenObject could now be invalid.   

當對象從基本集合類移除,集合類發送一個release(而不是autorelease)消息。如果集合類是對象的唯一擁有者,被移除的對象(例子中heisenObject)就被立即銷燬。

 

2.父對象被銷燬

  1. id parent = <#create a parent object#>;   
  2. // ...   
  3. heisenObject = [parent child] ;   
  4. [parent release]; // Or, for example: self.parent = nil;   
  5. // heisenObject could now be invalid.   

 某些情況,開發者從其他對象獲得一個對象,直接或見解釋放父對象。release父對象導致被銷燬,父對象又是子對象的唯一擁有者,子對象將同時被銷燬。

 

防止這些情況,當開發者收到heisenObject時retain,使用完release掉,比如:

  1. heisenObject = [[array objectAtIndex:n] retain];   
  2. [array removeObjectAtIndex:n];   
  3. // Use heisenObject...   
  4. [heisenObject release];   

 

不要對稀缺資源進行dealloc

不要在dealloc函數中管理file descriptor、network connections、buffer和caches這些資源。通常,開發者不應設計帶有dealloc這樣的類。dealloc可能延遲調用,要麼就成爲程序的一個bug或者造成程序崩潰。

 

相反,如果你有一個稀缺資源的類,你應該這樣設計應用程序,例如,你知道當你不再需要的資源的時候,然後可以告訴實例clean up。通常,你會再釋放該實例,緊接着調用dealloc,你不會受到額外的問題。

 

如果您嘗試在dealloc中背馱式得資源管理,可能會出現問題。

1.順序依賴被拆散。雖然開發者可能希望得到一個特定順序,被拆散的對象圖本質上是無序的。如果對象是被autorelease的,被拆散的順序可能還有變化,也可能導致意想不到的結果。

2.非回收式的稀缺資源。內存泄露是可以修復bug,內存泄露的傷害不是立即致命的。如果稀缺資源在不能釋放的時候,被你釋放了,你可能會碰到更嚴重的問題。如果你的應用程序使用文件描述符(file descriptor),可能導致不能寫數據。

3.在錯誤的線程中執行cleanup邏輯。如果一個對象被開發者設置爲是autorelease的,它會被任意一個“它正好存在於的”線程的自動釋放池給釋放掉。這是很容易的致命錯誤:該資源應該在一個線程中使用和釋放。

(If an object is autoreleased at an unexpected time, it will be deallocated on whatever thread’s autorelease pool block it happens to be in. This can easily be fatal for resources that should only be touched from one thread)

 

集合擁有它所包含的對象

當你添加一個對象到集合(如,array,dictionary和set),集合獲得對象的所有權。對象被移除的時候或者集合本身release的時候,放棄對象的所有權。如果開發者想創建一個帶有粒度的array,可以這麼搞:

  1. NSMutableArray *array = <#Get a mutable array#>;   
  2. NSUInteger i;   
  3. // ...   
  4. for (i = 0; i < 10; i++) {   
  5.     NSNumber *convenienceNumber = [NSNumber numberWithInteger:i];   
  6.     [array addObject:convenienceNumber];   
  7. }   

 

這種情況,開發者沒有調用alloc,所以無需掉用release。也沒有必要retain新的convenienceNumber。

  1. NSMutableArray *array = <#Get a mutable array#>;   
  2. NSUInteger i;   
  3. // ...   
  4. for (i = 0; i < 10; i++) {   
  5.     NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i];   
  6.     [array addObject:allocedNumber];   
  7.     [allocedNumber release];   
  8. }   

這個情況,開發者需要向allocedNumber發送一個release消息在for的作用域之內,來匹配alloc。既然array的addObject方法 retain了allocedNumber,allocedNumber就會被array管理刪除。

 

要像理解這些,把自己當做設計集合類的開發者們。你想保證:即使不在你照看下,集合中的變量能仍然好用,所以,當對象傳入的時候,你retain了一次。當對象移除集合類,你需要發送release消息。

 

對象所有權策略是基於引用計數實現的

對象所有權的策略是通過引用計數——通常叫做retain count實現的。每一個對象有一個retaincount變量。

1.創建對象後,它的retaincount是1

2. retain之後,retain count +1

3.release之後 retain count -1 

4.autorelease之後,在自動釋放池最後-1

5.對象的retain count減少到0的時候,對象被銷燬。

 

【重要】

不要顯式調用對象的retainCount,結果往往具有誤導性,作爲開發者可能不瞭解框架式如何對對象retain的。在調試內存管理中,你應該只關注確保你的代碼遵循所有權規則。

 

原文:Practical Memory Management


關於iOS內存管理

應用程序內存管理是:“程序運行時,開闢的內存空間。使用它,釋放它”的過程,寫的好的程序儘可能少使用內存。在Objective-C中,內存管理被看做是:“在很多數據、代碼下,分配受限內存資源所有權方法”。當你依據這個指南完成你的程序時,你將獲得“通過顯式管理對象的命週期,不使用的時候釋放他們,來管理程序內存”的知識。

 

儘管,典型的內存管理是作用於單個對象,你的目標是通過管理對象圖。你想確保:在內存中沒有比實際需要的還多的對象。

概述

Objective-C提供兩種內存管理的方法:

1.“manual-release”(MRR),需要顯式管理內存通過跟蹤對象的所有權。MRR基於NSObject類在運行時提供的引用計數實現的。

2.“Automatic Reference Counting”ARC,系統使用相同的引用計數基於MRR,但是在編譯時,爲開發者適當插入一些內存管理方法。強烈建議開發者在新項目中使用ARC。使用ARC就無需理解本文所描述的內容了。

 

防止內存相關問題的好的做法

兩個主要的內存管理誤用問題

1.釋放或覆蓋正在使用的數據。這將造成內存損壞,造成應用程序崩潰,或者更壞的情況是損壞用戶數據。

2.沒有釋放數據,導致內存泄露。泄漏導致應用程序的內存使用量逐漸增加,這反過來又可能會導致系統性能較差或者應用程序被終止(crash)

 

引用計數內存管理的角度思考,但是,往往是適得其反,因爲你往往會考慮內存管理方面的實現細則,而不是在你的實際目標。相反,你應該想到的內存管理對象所有權和對象圖的角度。

1.Cocoa使用簡單的命名慣例來指示,是否擁有函數返回的對象。(點擊查看內存管理策略

2.儘管內存管理基本策略很簡單,有一些實際的步驟,你可以使內存管理更輕鬆,有助於確保你的程序仍然可靠和穩定的,而在同一時間最大限度地減少資源需求。(點擊查看內存管理實踐

3.Autorelease pool 提供一種機制:讓對象延遲release。這個對你想放棄所有權,但又想避免立即釋放(比如函數的返回值)。有些時候,你可能會使用自己的autorelease池塊。(點擊查看自動釋放池)。

 

使用分析工具來調試內存問題

在編譯時候找出代碼的問題。使用Xcode內嵌的Clang Static Analyzer 。

如果內存管理的問題仍然發生,還有其他的工具和技術,你可以用它來識別和診斷問題。

1.多數工具和技術都在TN2239中有描述,iOS Debugging Magic 特別是NSZombie來幫助找到過度釋放對象。

2.使用Instruments來追蹤引用計數事件並找到內存泄露。( 參考 Collecting Data on Your App

 

原文:About Memory Management

發佈了1 篇原創文章 · 獲贊 0 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章