iOS - 老生常談內存管理(二):從 MRC 提及

前言

  MRC全稱Manual Reference Counting,也稱爲MRRmanual retain-release),手動引用計數內存管理,即開發者須要手動控制對象的引用計數來管理對象的內存。
  在MRC年代,咱們常常須要寫retainreleaseautorelease等方法來手動管理對象內存,然而這些方法在ARC是禁止調用的,調用會引發編譯報錯。
  下面咱們從MRC提及,聊聊iOS內存管理。html

簡介

關於內存管理

  應用程序內存管理是在程序運行時分配內存,使用它並在使用完後釋放它的過程。編寫良好的程序將使用盡量少的內存。在 Objective-C 中,它也能夠看做是在許多數據和代碼之間分配有限內存資源全部權的一種方式。掌握內存管理知識,咱們就能夠很好地管理對象生命週期並在再也不須要它們時釋放它們,從而管理應用程序的內存。
  雖然一般在單個對象級別上考慮內存管理,但實際上咱們的目標是管理對象圖,要保證在內存中只保留須要用到的對象,確保沒有發生內存泄漏。
  下圖是蘋果官方文檔給出的 「內存管理對象圖」,很好地展現了一個對象 「建立——持有——釋放——銷燬」 的過程。 編程

Objective-C 在iOS中提供了兩種內存管理方法:數組

  1. MRC,也是本篇文章要講解的內容,咱們經過跟蹤本身持有的對象來顯式管理內存。這是使用一個稱爲 「引用計數」 的模型來實現的,由 Foundation 框架的 NSObject 類與運行時環境一塊兒提供。緩存

  2. ARC,系統使用與MRC相同的引用計數系統,可是它會在編譯時爲咱們插入適當的內存管理方法調用。使用ARC,咱們一般就不須要了解本文章中描述的MRC的內存管理實現,儘管在某些狀況下它可能會有所幫助。可是,做爲一名合格的iOS開發者,掌握這些知識是頗有必要的。安全

良好的作法可防止與內存相關的問題

  • 不正確的內存管理致使的問題主要有兩種:
    ① 釋放或覆蓋仍在使用的數據
    這會致使內存損壞,而且一般會致使應用程序崩潰,甚至損壞用戶數據。
    ② 不釋放再也不使用的數據會致使內存泄漏
    內存泄漏是指沒有釋放已分配的內存,即便它再也不被使用。內存泄漏會致使應用程序不斷增長內存使用量,進而可能致使系統性能降低或應用程序被終止。網絡

  • 可是,從引用計數的角度考慮內存管理一般會拔苗助長,由於你會傾向於根據實現細節而不是實際目標來考慮內存管理。相反,你應該從對象全部權和對象圖的角度考慮內存管理。多線程

  • Cocoa 使用簡單的命名約定來指示你什麼時候持有由方法返回的對象。(請參閱 《內存管理策略》 章節)app

  • 儘管內存管理基本策略很簡單,可是你能夠採起一些措施來簡化內存管理,並幫助確保程序保持可靠和健壯,同時最大程度地減小其資源需求。(請參閱 《實用內存管理》 章節)框架

  • 自動釋放池塊提供了一種機制,你能夠經過該機制向對象發送 「延遲」release消息。這在須要放棄對象全部權但又但願避免當即釋放對象的狀況下頗有用(例如從方法返回對象時)。在某些狀況下,你可能會使用本身的自動釋放池塊。(請參閱 《使用 Autorelease Pool Blocks》 章節)工具

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

爲了在編譯時發現代碼問題,可使用 Xcode 內置的 Clang Static Analyzer。 若是仍然出現內存管理問題,則可使用其餘工具和技術來識別和診斷問題。

內存管理策略

NSObject 協議中定義的內存管理方法與遵照這些方法命名約定的自定義方法的組合提供了用於引用計數環境中的內存管理的基本模型。NSObject 類還定義了一個dealloc方法,該方法在對象被銷燬時自動調用。

基本內存管理規則

  在MRC下,咱們要嚴格遵照引用計數內存管理規則。
  內存管理模型基於對象全部權。任何對象均可以擁有一個或多個全部者。只要一個對象至少擁有一個全部者,它就會繼續存在。若是對象沒有全部者,則運行時系統會自動銷燬它。爲了確保你清楚本身什麼時候擁有和不擁有對象的全部權,Cocoa 設置瞭如下策略:

四條規則

  • 建立並持有對象
    使用 alloc/new/copy/mutableCopy 等方法(或者以這些方法名開頭的方法)建立的對象咱們直接持有,其RC(引用計數,如下使用統一使用RC)初始值爲 1,咱們直接使用便可,在不須要使用的時候調用一下release方法進行釋放。
id obj = [NSObject alloc] init]; // 建立並持有對象,RC = 1
    /* * 使用該對象,RC = 1 */
    [obj release]; // 在不須要使用的時候調用 release,RC = 0,對象被銷燬
複製代碼

  若是咱們經過自定義方法 建立並持有對象,則方法名應該以 alloc/new/copy/mutableCopy 開頭,且應該遵循駝峯命名法規則,返回的對象也應該由這些方法建立,如:

- (id)allocObject
{
    id obj = [NSObject alloc] init];
    retain obj;
}
複製代碼

  能夠經過retainCount方法查看對象的引用計數值。

NSLog(@"%ld", [obj retainCount]);
複製代碼
  • 可使用 retain 持有對象
    咱們可使用retain對一個對象進行持有。使用上述方法之外的方法建立的對象,咱們並不持有,其RC初始值也爲 1。可是須要注意的是,若是要使用(持有)該對象,須要先進行retain,不然可能會致使程序Crash。緣由是這些方法內部是給對象調用了autorelease方法,因此這些對象會被加入到自動釋放池中。

  ① 狀況一:iOS 程序中不手動指定@autoreleasepool
  當RunLoop迭代結束時,會自動給自動釋放池中的對象調用release方法。因此若是咱們使用前不進行retain,若是RunLoop迭代結束,對象調用release方法其RC值就會變成 0,該對象就會被銷燬。若是咱們這時候訪問已經被銷燬的對象,程序就會Crash

/* 正確的用法 */

    id obj = [NSMutableArray array]; // 建立對象但並不持有,對象加入自動釋放池,RC = 1

    [obj retain]; // 使用以前進行 retain,對對象進行持有,RC = 2
    /* * 使用該對象,RC = 2 */
    [obj release]; // 在不須要使用的時候調用 release,RC = 1
    /* * RunLoop 可能在某一時刻迭代結束,給自動釋放池中的對象調用 release,RC = 0,對象被銷燬 * 若是這時候 RunLoop 還未迭代結束,該對象還能夠被訪問,不過這是很是危險的,容易致使 Crash */
複製代碼

  ② 狀況二:手動指定@autoreleasepool
  這種狀況就更加明顯了,若是@autoreleasepool做用域結束,就會自動給autorelease對象調用release方法。若是這時候咱們再訪問該對象,程序就會崩潰EXC_BAD_ACCESS

/* 錯誤的用法 */

    id obj;
    @autoreleasepool {
        obj = [NSMutableArray array]; // 建立對象但並不持有,對象加入自動釋放池,RC = 1
    } // @autoreleasepool 做用域結束,對象 release,RC = 0,對象被銷燬
    NSLog(@"%@",obj); // EXC_BAD_ACCESS
複製代碼
/* 正確的用法 */

    id obj;
    @autoreleasepool {
        obj = [NSMutableArray array]; // 建立對象但並不持有,對象加入自動釋放池,RC = 1
        [obj retain]; // RC = 2
    } // @autoreleasepool 做用域結束,對象 release,RC = 1
    NSLog(@"%@",obj); // 正常訪問
    /* * 使用該對象,RC = 1 */
    [obj release]; // 在不須要使用的時候調用 release,RC = 0,對象被銷燬
複製代碼

  若是咱們經過自定義方法 建立但並不持有對象,則方法名就不該該以 alloc/new/copy/mutableCopy 開頭,且返回對象前應該要先經過autorelease方法將該對象加入自動釋放池。如:

- (id)object
{
    id obj = [NSObject alloc] init];
    [obj autorelease];
    retain obj;
}
複製代碼

  這樣調用方在使用該方法建立對象的時候,他就會知道他不持有該對象,因而他會在使用該對象前進行retain,並在不須要該對象時進行release

備註releaseautorelease的區別:

  • 調用release,對象的RC會當即 -1;
  • 調用autorelease,對象的RC不會當即 -1,而是將對象添加進自動釋放池,它會在一個恰當的時刻自動給對象調用release,因此autorelease至關於延遲了對象的釋放。
  • 再也不須要本身持有的對象時釋放
    在不須要使用(持有)對象的時候,須要調用一下release或者autorelease方法進行釋放(或者稱爲 「放棄對象使用權」),使其RC-1,防止內存泄漏。當對象的RC爲 0 時,就會調用dealloc方法銷燬對象。

  • 不能釋放非本身持有的對象
    從以上咱們能夠得知,持有對象有兩種方式,一是經過 alloc/new/copy/mutableCopy 等方法建立對象,二是經過retain方法。若是本身是持有者,那麼在不須要該對象的時候須要調用一下release方法進行釋放。可是,若是本身不是持有者,就不能對對象進行release,不然會發生程序崩潰EXC_BAD_ACCESS,以下兩種狀況:

id obj = [[NSObject alloc] init]; // 建立並持有對象,RC = 1
    [obj release]; // 若是本身是持有者,在不須要使用的時候調用 release,RC = 0
    /* * 此時對象已被銷燬,不該該再對其進行訪問 */
    [obj release]; // EXC_BAD_ACCESS,這時候本身已經不是持有者,再 release 就會 Crash
    /* * 再次 release 已經銷燬的對象(過分釋放),或是訪問已經銷燬的對象都會致使崩潰 */
複製代碼
id obj = [NSMutableArray array]; // 建立對象,但並不持有對象,RC = 1
    [obj release]; // EXC_BAD_ACCESS 雖然對象的 RC = 1,可是這裏並不持有對象,因此致使 Crash
複製代碼

  還有一種狀況,這是不容易發現問題的狀況。下面程序運行竟然不會崩潰?這是爲何呢?這裏要介紹兩個概念,野指針殭屍對象

  • 野指針: 在 C 中是指沒有進行初始化的指針,該指針指向一個隨機的空間,它的值是個垃圾值;在 OC 中是指指向的對象已經被回收了的指針(網上不少都是這樣解釋,但我認爲它應該叫 「懸垂指針」 纔對)。
  • 殭屍對象: 指已經被銷燬的對象,但這個對象所佔的內存空間尚未分配給別人。

  以下這種狀況,當咱們經過野指針去訪問殭屍對象的時候,可能會有問題,也可能沒有問題。若是殭屍對象所佔的空間尚未分配給別人,這時候訪問沒有問題,若是已經分配給了別人,再次訪問就會崩潰。

Person *person = [[Person alloc] init]; // 建立並持有對象,RC = 1
    [person release]; // 若是本身是持有者,在不須要使用的時候調用 release,RC = 0
    [person release]; // 這時候 person 指針爲野指針,對象爲殭屍對象
    [person release]; // 可能這時候殭屍對象所佔的空間尚未分配給別人,因此能夠正常訪問
複製代碼

  以上幾個例子均可以用一句話總結:不能釋放非本身持有的對象。

以上就是內存管理基本的四條規則,你對照上篇文章中講的《辦公室裏的照明問題》,是否是就比較好理解了,你細品,你細細的品!

一個簡單的例子

Person 對象是使用alloc方法建立的,所以在不須要該對象時發送一條release消息。

{
    Person *aPerson = [[Person alloc] init];
    // ...
    NSString *name = aPerson.fullName;
    // ...
    [aPerson release];
}
複製代碼

使用 autorelease 發送延遲 release

當你須要發送延遲release消息時,可使用autorelease,一般用在從方法返回對象時。例如,你能夠像這樣實現 fullName 方法:

- (NSString *)fullName {
    NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@",
                                          self.firstName, self.lastName] autorelease];
    return string;
}
複製代碼

根據內存管理規則,你經過alloc方法建立並持有對象,要在不須要該對象時發送一條release消息。可是若是你在方法中使用release,則return以前就會銷燬 NSString 對象,該方法將返回無效對象。使用autorelease,就會延遲release,在 NSString 對象被釋放以前返回。

你還能夠像這樣實現 fullName 方法:

- (NSString *)fullName {
    NSString *string = [NSString stringWithFormat:@"%@ %@",
                                 self.firstName, self.lastName];
    return string;
}
複製代碼

根據內存管理規則,你不持有 NSString 對象,所以你不用擔憂它的釋放,直接return便可。stringWithFormat 方法內部會給 NSString 對象調用autorelease方法。

相比之下,如下實現是錯誤的:

- (NSString *)fullName {
    NSString *string = [[NSString alloc] initWithFormat:@"%@ %@",
                                         self.firstName, self.lastName];
    return string;
}
複製代碼

在 fullName 方法內部咱們經過alloc方法建立對象並持有,然而並無釋放對象。而該方法名不以 alloc/new/copy/mutableCopy 等開頭。在調用方看來,經過該方法得到的對象並不持有,所以他會進行retain並在他不須要該對象時release,在他看來這樣使用該對象沒有內存問題。然而這時候該對象的引用計數爲 1,並無銷燬,就發生了內存泄漏。

你不持有經過引用返回的對象

Cocoa 中的一些方法指定經過引用返回對象(它們採用ClassName **id *類型的參數)。常見的就是使用NSError對象,該對象包含有關錯誤的信息(若是發生錯誤),如initWithContentsOfURL:options:error:NSData)和initWithContentsOfFile:encoding:error:NSString)方法等。

在這些狀況下,也聽從內存管理規則。當你調用這些方法時,你不會建立該NSError對象,所以你不持有該對象,也無需釋放它,如如下示例所示:

NSString *fileName = <#Get a file name#>;
    NSError *error;
    NSString *string = [[NSString alloc] initWithContentsOfFile:fileName
                            encoding:NSUTF8StringEncoding error:&error];
    if (string == nil) {
        // Deal with error...
    }
    // ...
    [string release];
複製代碼

實現 dealloc 以放棄對象的全部權

NSObject 類定義了一個dealloc方法,該方法會在一個對象沒有全部者(RC=0)而且它的內存被回收時由系統自動調用 —— 在 Cocoa 術語中稱爲freeddeallocateddealloc方法的做用是銷燬對象自身的內存,並釋放它持有的任何資源,包括任何實例變量的全部權。

如下舉了一個在 Person 類中實現 dealloc方法的示例:

@interface Person : NSObject
@property (retain) NSString *firstName;
@property (retain) NSString *lastName;
@property (assign, readonly) NSString *fullName;
@end
 
@implementation Person
// ...
- (void)dealloc
    [_firstName release];
    [_lastName release];
    [super dealloc];
}
複製代碼

注意:

  • 切勿直接調用另外一個對象dealloc的方法;
  • 你必須在實現結束時調用[super dealloc]
  • 你不該該將系統資源的管理與對象生命週期聯繫在一塊兒,請參閱《不要使用 dealloc 管理稀缺資源》章節;
  • 當應用程序終止時,可能不會向對象發送dealloc消息。由於進程的內存在退出時會自動清除,因此讓操做系統清理資源比調用全部對象的dealloc方法更有效。

Core Foundation 使用類似但不一樣的規則

Core Foundation 對象有相似的內存管理規則(請參閱 《 Core Foundation 內存管理編程指南》)。可是,Cocoa 和 Core Foundation 的命名約定不一樣。特別是 Core Foundation 的建立對象的規則(請參閱 《The Create Rule》)不適用於返回 Objective-C 對象的方法。例如如下的代碼片斷,你不負責放棄 myInstance 的全部權。由於在 Cocoa 中使用 alloc/new/copy/mutableCopy 等方法(或者以這些方法名開頭的方法)建立的對象,咱們才須要對其進行釋放。

MyClass * myInstance = [MyClass createInstance];
複製代碼

實用內存管理

儘管內存管理基本策略很簡單,可是你能夠採起一些措施來簡化內存管理,並幫助確保程序保持可靠和健壯,同時最大程度地減小其資源需求。

使用訪問器方法讓內存管理更輕鬆

若是類中有對象類型的屬性,則你必須確保在使用過程當中該屬性賦值的對象不被釋放。所以,在賦值對象時,你必須持有對象的全部權,讓其引用計數加 1。還必需要把當前持有的舊對象的引用計數減 1。

有時它可能看起來很乏味或繁瑣,但若是你始終使用訪問器方法,那麼內存管理出現問題的可能性會大大下降。若是你在整個代碼中對實例變量使用retainrelease,這確定是錯誤的作法。

如下在 Counter 類中定義了一個NSNumber對象屬性。

@interface Counter : NSObject
@property (nonatomic, retain) NSNumber *count;
@end;
複製代碼

@property會自動生成settergetter方法的聲明,一般,你應該使用@synthesize讓編譯器合成方法。但若是咱們瞭解訪問器方法的實現是有益的。

@synthesize會自動生成settergetter方法的實現以及下劃線實例變量,詳細的解釋將在下一篇ARC文章中講到。

getter方法只須要返回合成的實例變量,因此不用進行retainrelease

- (NSNumber *)count {
    return _count;
}
複製代碼

setter方法中,若是其餘全部人都遵循相同的規則,那麼其餘人極可能隨時讓新對象 newCount 的引用計數減 1,從而致使 newCount 被銷燬,因此你必須對其retain使其引用計數加 1。你還必須對舊對象release以放棄對它的持有。因此,先對新對象進行retain,再對舊對象進行release,而後再進行賦值操做。(在Objective-C中容許給nil發送消息,且這樣會直接返回不作任何事情。因此就算是第一次調用,_count 變量爲nil,對其進行 release也沒事。能夠參閱《深刻淺出 Runtime(三):消息機制》

注意: 你必須先對新對象進行retain,再對舊對象進行release。順序顛倒的話,若是新舊對象是同一對象,則可能會發生意外致使對象dealloc

- (void)setCount:(NSNumber *)newCount {
    [newCount retain];
    [_count release];
    // Make the new assignment.
    _count = newCount;
}
複製代碼

以上是蘋果官方的作法,該作法在性能上略有不足,若是新舊對象是同一個對象,就存在沒必要要的方法調用。

更好的作法以下:先判斷新舊對象是不是同一個對象,若是是的話就什麼都不作;若是新舊對象不是同一個對象,則對舊對象進行release,對新對象進行retain並賦值給合成的實例變量。

- (void)setCount:(NSNumber *)newCount {
    if (_count != newCount) {
        [_count release];
        _count = [newCount retain];
    }
}
複製代碼

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

假設咱們要重置以上count屬性的值。有如下兩種方法:

- (void)reset {
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [self setCount:zero];
    [zero release];
}
複製代碼
- (void)reset {
    NSNumber *zero = [NSNumber numberWithInteger:0];
    [self setCount:zero];
}
複製代碼

對於簡單的狀況,咱們還能夠像下面這樣直接操做_count變量,但這樣作早晚會發生錯誤(例如,當你忘記retainrelease,或者實例變量的內存管理語義(即屬性關鍵字)發生更改時)。

- (void)reset {
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [_count release];
    _count = zero;
}
複製代碼

另外請注意,若是使用KVO,則以這種方式更改變量不會觸發KVO監聽方法。關於KVO我作了比較全面的總結,能夠參閱《iOS - 關於 KVO 的一些總結》

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

你不該該在初始化方法和dealloc中使用訪問器方法來設置實例變量,而是應該直接操做實例變量。

例如,咱們要在初始化 Counter 對象時,初始化它的count屬性。正確的作法以下:

- init {
    self = [super init];
    if (self) {
        _count = [[NSNumber alloc] initWithInteger:0];
    }
    return self;
}
複製代碼
- initWithCount:(NSNumber *)startingCount {
    self = [super init];
    if (self) {
        _count = [startingCount copy];
    }
    return self;
}
複製代碼

因爲 Counter 類具備實例變量,所以還必須實現dealloc方法。在該方法中經過向它們發送release消息來放棄任何實例變量的全部權,並在最後調用super的實現:

- (void)dealloc {
    [_count release];
    [super dealloc];
}
複製代碼

以上是蘋果官方的作法。推薦作法以下,在release以後再對 _count 賦值nil

備註
先解釋一下nilrelease的做用:nil是將一個對象的指針置爲空,只是切斷了指針和內存中對象的聯繫,並無釋放對象內存;而release纔是真正釋放對象內存的操做。
之因此在release以後再對 _count 賦值nil,是爲了防止 _count 在被銷燬以後再次被訪問而致使Crash

- (void)dealloc {
    [_count release];
    _count = nil;
    [super dealloc];
}
複製代碼

咱們也能夠在dealloc經過self.count = nil;一步到位,由於一般它至關於[_count release];_count = nil;兩步操做。可是蘋果說了,不建議咱們在dealloc中使用訪問器方法。

- (void)dealloc {
    self.count = nil;
    [super dealloc];
}
複製代碼

Why? 爲何初始化方法中須要self = [super init]

  • 先大概解釋一下selfsuperself是對象指針,指向當前消息接收者。super是編譯器指令,使用super調用方法是從當前消息接收者類的父類中開始查找方法的實現,但消息接收者仍是子類。有關selfsuper的詳細解釋能夠參閱《深刻淺出 Runtime(四):super 的本質》
  • 調用[super init],是子類去調用父類的init方法,先完成父類的初始化工做。要注意調用過程當中,父類的init方法中的self仍是子類。
  • 執行self = [super init],若是父類初始化成功,接下來就進行子類的初始化;若是父類初始化失敗,則[super init]會返回nil並賦值給self,接下來if (self)語句的內容將不被執行,子類的init方法也返回nil。這樣作能夠防止由於父類初始化失敗而返回了一個不可用的對象。若是你不是這樣作,你可能你會獲得一個不可用的對象,而且它的行爲是不可預測的,最終可能會致使你的程序發生Crash

Why? 爲何不要在初始化方法和 dealloc 中使用訪問器方法?

  • 在初始化方法和dealloc中,對象的存在與否還不肯定,它可能還未初始化完畢,因此給對象發消息可能不會成功,或者致使一些問題的發生。
    • 進一步解釋,假如咱們在init中使用setter方法初始化實例變量。在init中,咱們會調用self = [super init]對父類的東西先進行初始化,即子類先調用父類的init方法(注意: 調用的父類的init方法中的self仍是子類對象)。若是父類的init中使用setter方法初始化實例變量,且子類重寫了該setter方法,那麼在初始化父類的時候就會調用子類的setter方法。而此時只是在進行父類的初始化,子類初始化還未完成,因此可能會發生錯誤。
    • 在銷燬子類對象時,首先是調用子類的dealloc,最後調用[super dealloc](這與init相反)。若是在父類的dealloc中調用了setter方法且該方法被子類重寫,就會調用到子類的setter方法,但此時子類已經被銷燬,因此這也可能會發生錯誤。
    • 《Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法》書中的第 31 條 —— 在 dealloc 方法中只釋放引用並解除監聽 一文中也提到:在 dealloc 裏不要調用屬性的存取方法,由於有人可能會覆寫這些方法,並於其中作一些沒法在回收階段安全執行的操做。此外,屬性可能正處於 「鍵值觀測」(Key-Value Observation,KVO)機制的監控之下,該屬性的觀察者(observer)可能會在屬性值改變時 「保留」 或使用這個即將回收的對象。這種作法會令運行期系統的狀態徹底失調,從而致使一些莫名其妙的錯誤。
    • 綜上,錯誤的緣由由繼承和子類重寫訪問器方法引發。在初始化方法和 dealloc 中使用訪問器方法的話,若是存在繼承且子類重寫了訪問器方法,且在方法中作了一些其它操做,就頗有可能發生錯誤。雖然通常狀況下咱們可能不會同時知足以上條件而致使錯誤,可是爲了不錯誤的發生,咱們仍是規範編寫代碼比較好。
  • 性能降低。特別是,若是屬性是atomic的。
  • 可能產生反作用。如使用KVO的話會觸發KVO等。

不過,有些狀況咱們必須破例。好比:

  • 待初始化的實例變量聲明在父類中,而咱們又沒法在子類中訪問此實例變量的話,那麼咱們在初始化方法中只能經過setter來對實例變量賦值。

使用弱引用來避免 Retain Cycles

retain對象會建立對該對象的強引用(即引用計數 +1)。一個對象在release它的全部強引用以後(即引用計數 =0)纔會dealloc。若是兩個對象相互retain強引用,或者多個對象,每一個對象都強引用下一個對象直到回到第一個,就會出現 「Retain Cycles(循環引用)」 問題。循環引用會致使它們中的任何對象都沒法dealloc,就產生了內存泄漏。

舉個例子,Document 對象中有一個屬性 Page 對象,每一個 Page 對象都有一個屬性,用於存儲它所在的 Document。若是 Document 對象具備對 Page 對象的強引用,而且 Page 對象具備對 Document 對象的強引用,則它們都不能被銷燬。

Retain Cycles」 問題的解決方案是使用弱引用。弱引用是非持有關係,對象do not retain它引用的對象。

MRC中,這裏的 「弱引用」 是指do not retain,而不是ARC中的weak

可是,爲了保持對象圖無缺無損,必須在某處有強引用(若是隻有弱引用,則 Page 對象和 Paragraph 對象可能沒有任何全部者,所以將被銷燬)。所以,Cocoa 創建了一個約定,即父對象應該對其子對象保持強引用(retain),而子對象應該對父對象保持弱引用(do not retain)。

所以,Document 對象具備對其 Page 對象的強引用,但 Page 對象對 Document 對象是弱引用,以下圖所示:

Cocoa 中弱引用的示例包括但不限於 table data sources、outline view items、notification observers 以及其餘 targets 和 delegates。

當你向只持有弱引用的對象發送消息時,須要當心。若是在對象銷燬後向其發送消息就會Crash。你必須定義好何時對象是有效的。在大多數狀況下,弱引用對象知道其它對象對它的弱引用,就像循環引用的狀況同樣,你要負責在弱引用對象銷燬時通知其它對象。例如,當你向通知中心註冊對象時,通知中心會存儲對該對象的弱引用,並在發佈相應的通知時向其發送消息。在對象要銷燬時,你須要在通知中心註銷它,以防止通知中心向已銷燬的對象發送消息。一樣,當 delegate 對象銷燬時,你須要向委託對象發送setDelegate: nil消息來刪除 delegate 引用。這些消息一般在對象的 dealloc 方法中發送。

避免致使你正在使用的對象被銷燬

Cocoa 的全部權策略指定,對象做爲方法參數傳入,其在調用的方法的整個範圍內保持有效,也能夠做爲方法的返回值返回,而沒必要擔憂它被釋放。對於應用程序來講,對象的 getter 方法返回緩存的實例變量或計算值並不重要。重要的是對象在你須要的時間內保持有效。

此規則偶爾會有例外狀況,主要分爲兩類。

  1. 從一個基本集合類中刪除對象時。
heisenObject = [array objectAtIndex:n];
    [array removeObjectAtIndex:n];
    // heisenObject could now be invalid.
複製代碼

當一個對象從一個基本集合類中移除時,它將被髮送一條release(而不是autorelease)消息。若是集合是移除對象的惟一全部者,則移除的對象(示例中的 heisenObject)將當即被銷燬。

  1. 當 「父對象」 被銷燬時。
id parent = <#create a parent object#>;
    // ...
    heisenObject = [parent child] ;
    [parent release]; // Or, for example: self.parent = nil;
    // heisenObject could now be invalid.
複製代碼

在某些狀況下,你經過父對象得到子對象,而後直接或間接release父對象。若是release父對象致使它被銷燬,而且父對象是子對象的惟一全部者,則子對象(示例中的 heisenObject)將同時被銷燬(假設在父對象的dealloc方法中,子對象被髮送一個release而不是一個autorelease消息)。

爲了防止這些狀況發生,在獲得 heisenObject 時retain它,並在完成後release它。例如:

heisenObject = [[array objectAtIndex:n] retain];
    [array removeObjectAtIndex:n];
    // Use heisenObject...
    [heisenObject release];
複製代碼

不要使用 dealloc 來管理稀缺資源

你一般不該該在dealloc方法中管理稀缺資源,如文件描述符,網絡鏈接和緩衝區或緩存等。特別是,你不該該設計類,以便在你想讓系統調用dealloc時就調用它。因爲bug或應用程序崩潰,dealloc的調用可能會被延遲或未調用。

相反,若是你有一個類的實例管理稀缺的資源,你應該在你再也不須要這些資源時讓該實例釋放這些資源。而後,你一般會release該實例,緊接着它dealloc。若是該實例的dealloc沒有被及時調用或者未調用,你也不會遇到稀缺資源不被及時釋放或者未釋放的問題,由於此前你已經釋放了資源。

若是你嘗試在dealloc上進行資源管理,則可能會出現問題。例如:

  1. 依賴對象圖的釋放機制。
    對象圖的釋放機制本質上是無序的。儘管一般你但願能夠按照特定的順序釋放,可是會讓程序變得很脆弱。若是對象被autorelease而不是release,則釋放順序可能會改變,這可能會致使意外的結果。

  2. 不回收稀缺資源。
    內存泄漏是應該被修復的bug,但它們一般不會當即致命。然而,若是在你但願釋放稀缺資源時沒有釋放,則可能會遇到更嚴重的問題。例如,若是你的應用程序用完了文件描述符,則用戶可能沒法保存數據。

  3. 釋放資源的操做被錯誤的線程執行。
    若是一個對象在一個意外的時間調用了autorelease,它將在它碰巧進入的任何一個線程的自動釋放池塊中被釋放。對於只能從一個線程觸及的資源來講,這很容易致命。

集合持有它們包含的對象

將對象添加到集合(例如arraydictionaryset)時,集合將得到對象的全部權。當從集合中移除對象或集合自己被銷燬時,集合將放棄對象的全部權。所以,例如,若是要建立一個存儲numbers的數組,能夠執行如下任一操做:

NSMutableArray *array = <#Get a mutable array#>;
    NSUInteger i;
    // ...
    for (i = 0; i < 10; i++) {
        NSNumber *convenienceNumber = [NSNumber numberWithInteger:i];
        [array addObject:convenienceNumber];
    }
複製代碼

在這種狀況下,NSNumber對象不是經過alloc等建立,所以無需調用release。也不須要對NSNumber對象進行retain,由於數組會這樣作。

NSMutableArray *array = <#Get a mutable array#>;
    NSUInteger i;
    // ...
    for (i = 0; i < 10; i++) {
        NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i];
        [array addObject:allocedNumber];
        [allocedNumber release];
    }
複製代碼

在這種狀況下,你就須要對NSNumber對象進行release。數組會在addObject:時對NSNumber對象進行retain,所以在數組中它不會被銷燬。

要理解這一點,能夠站在實現集合類的人的角度。你要確保在集合中它們不會被銷燬,因此你在它們添加進集合時給它們發送一個retain消息。若是刪除了它們,則必須給它們發送一個release消息。在集合的dealloc方法中,應該向集合中全部剩餘的對象發送一條release消息。

全部權策略是經過使用 Retain Counts 實現的

全部權策略經過引用計數實現的,引用計數也稱爲「retain count」。每一個對象都有一個retain count

  • 建立對象時,其retain count爲 1。
  • 向對象發送retain消息時,其retain count將 +1。
  • 向對象發送release消息時,其retain count將 -1。
  • 向對象發送autorelease消息時,其retain count在當前自動釋放池塊結束時 -1。
  • 若是對象的retain count減小到 0,它將dealloc

重要提示: 不該該顯式詢問對象的retain count是多少。結果每每會產生誤導,由於你可能不知道哪些系統框架對象retain了你關注的對象。在調試內存管理問題時,你只須要遵照內存管理規則就好了。

備註: 關於這些方法的具體實現,請參閱《iOS - 老生常談內存管理(四):源碼分析內存管理方法》

使用 Autorelease Pool Blocks

自動釋放池塊提供了一種機制,讓你能夠放棄對象的全部權,但避免當即釋放它(例如從方法返回對象時)。一般,你不須要建立本身的自動釋放池塊,但在某些狀況下,你必須這樣作或者這樣作是有益的。

關於 Autorelease Pool Blocks

Autorelease Pool Blocks 使用@autoreleasepool標記,示例以下:

@autoreleasepool {
        // Code that creates autoreleased objects.
    }
複製代碼

@autoreleasepool的末尾,在塊中接收到autorelease消息的對象將被髮送一條release消息。對象在塊內每接收一次autorelease消息,就會被髮送一條release消息。 與任何其餘代碼塊同樣,@autoreleasepool能夠嵌套,可是你一般不會這樣作。

@autoreleasepool {
        // . . .
        @autoreleasepool {
            // . . .
        }
        . . .
    }
複製代碼

MRC下還可使用NSAutoreleasePool建立自動釋放池。不過建議使用@autoreleasepool,蘋果說它比NSAutoreleasePool快大約六倍。

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    // Code benefitting from a local autorelease pool.
    [pool release]; // [pool drain]
複製代碼

Cocoa 老是但願代碼在@autoreleasepool中執行,不然autorelease對象不會被release,致使內存泄漏。若是你在@autoreleasepool以外發送autorelease消息,Cocoa 會打印一個合適的錯誤消息。AppKit 和 UIKit 框架會在RunLoop每次事件循環迭代中建立並處理@autoreleasepool,所以,你一般沒必要本身建立@autoreleasepool,甚至不須要知道建立@autoreleasepool的代碼怎麼寫。

可是,有三種狀況可能會使用你本身的@autoreleasepool

  • ① 若是你編寫的程序不是基於 UI 框架的,好比說命令行工具;
  • ② 若是你編寫的循環中建立了大量的臨時對象; 你能夠在循環內使用@autoreleasepool在每次循環結束時銷燬這些對象。這樣能夠減小應用程序的最大內存佔用。
  • ③ 若是你建立了輔助線程。 一旦線程開始執行,就必須建立本身的@autoreleasepool;不然,你的應用程序將存在內存泄漏。(有關詳細信息,請參閱《Autorelease Pool Blocks 和線程》章節。

關於@autoreleasepool的底層原理,能夠參閱《iOS - 聊聊 autorelease 和 @autoreleasepool》

使用 Local Autorelease Pool Blocks 來減小峯值內存佔用量

許多程序建立autorelease的臨時對象。這些對象將添加到程序的內存佔用空間,直到塊結束。在許多狀況下,容許臨時對象累積直到當前事件循環迭代結束時,而不會致使過多的開銷。可是,在某些狀況下,你可能會建立大量臨時對象,這些對象會大大增長內存佔用,而且你但願更快地銷燬這些對象。在這時候,你就能夠建立本身的@autoreleasepool。在塊結束時,臨時對象被release,這可讓它們儘快dealloc,從而減小程序的內存佔用。

如下示例演示瞭如何在 for 循環中使用 local autorelease pool block。

NSArray *urls = <# An array of file URLs #>;
    for (NSURL *url in urls) {
 
        @autoreleasepool {
            NSError *error;
            NSString *fileContents = [NSString stringWithContentsOfURL:url
                                             encoding:NSUTF8StringEncoding error:&error];
            /* Process the string, creating and autoreleasing more objects. */
        }
    }
複製代碼

for 循環一次處理一個文件。在@autoreleasepool內發送autorelease消息的任何對象(例如 fileContents)在塊結束時release

@autoreleasepool以後,你應該將塊中任何autorelease對象視爲 「已銷燬」。不要向該對象發送消息或將其返回給你的方法調用者。若是你須要某個autorelease的臨時對象在@autoreleasepool結束以後依然可用,能夠經過在塊內對該對象發送retain消息,而後在塊以後將對其發送autorelease,以下示例所示:

– (id)findMatchingObject:(id)anObject {
 
    id match;
    while (match == nil) {
        @autoreleasepool {
 
            /* Do a search that creates a lot of temporary objects. */
            match = [self expensiveSearchForObject:anObject];
 
            if (match != nil) {
                [match retain]; /* Keep match around. */
            }
        }
    }
 
    return [match autorelease];   /* Let match go and return it. */
}
複製代碼

@autoreleasepool中給match對象發送一條retain消息,並在@autoreleasepool以後給其發送一條autorelease消息,延長了match對象的生命週期,容許它在while循環外接收消息,而且能夠返回給findMatchingObject:方法的調用方。

Autorelease Pool Blocks 和線程

Cocoa 應用程序中的每一個線程都維護本身的 autorelease pool blocks 棧。若是你寫的是一個僅基於 Foundation 的程序或者若是你使用子線程,則須要建立本身的@autoreleasepool。 若是你的應用程序或線程長期存在而且可能會產生大量的autorelease對象,則應使用@autoreleasepool(如 AppKit 和 UIKit 就在主線程建立了@autoreleasepool);不然,autorelease對象會不斷累積,致使你的內存佔用量不斷增長。若是你在子線程上沒有進行 Cocoa 調用,則不須要使用@autoreleasepool

注意: 若是你使用pthreadPOSIX thread)而不是使用NSThread建立子線程,那麼你就不能使用 Cocoa 除非 Cocoa 處於多線程模式。Cocoa 只有在detach它的第一個NSThread對象以後纔會進入多線程模式。要想在pthread建立的子線程上使用 Cocoa,你的應用程序必須先detach至少一個能夠當即退出的NSThread對象。你可使用NSThread的類方法isMultiThreaded測試 Cocoa 是否處於多線程模式。

相關文章
相關標籤/搜索