【譯】蘋果官方手冊:高級內(nèi)存管理編程手冊3:實際內(nèi)存管理

盡管在內(nèi)存管理方法中提到的概念并不復(fù)雜,但還是存在一些實用的步驟可以使內(nèi)存管理更加方便,并確保你的程序在實用最少的資源需求的時候的可靠性和穩(wěn)定性。

實用訪問方法使內(nèi)存管理更方便

當你的類擁有一個對象作為屬性時,你必須保證所有的對象在你還需要使用它們之前,不能被銷毀。所以你必須在設(shè)置對象的時候就聲明所有權(quán)。同時你必須確保放棄對即時引用的值的所有權(quán)。
有時候這看起來枯燥無味又墨守成規(guī),但如果你一貫地使用訪問方法,內(nèi)存管理出錯的幾率會大大降低。如果你一直是在你的代碼中使用retainrelease管理引用變量,那么很可能你已經(jīng)誤入歧途。
考慮一下如何定義一個可以記錄一種你想記錄的數(shù)據(jù)的計數(shù)類。
@interface Counter : NSObject @property (nonatomic, retain) NSNumber *count; @end;
其中的property(聲明和定義某個類的訪問方法的速記法,同時你可以自己實現(xiàn)這些方法來替代默認的訪問方法)聲明了兩個訪問方法。一般情況下,編譯器會自動實現(xiàn)這些方法;然而,了解這些方法究竟是怎樣被實現(xiàn)的是很有意義的。

在“get”方法中,你僅僅返回引用變量,所以不需要使用retainrelease
- (NSNumber *)count { return _count; }

在“set”方法中,我們假定新的計數(shù)變量可能會在任意時刻被回收,所以需要發(fā)送一條retain消息確保它不會被回收掉,假設(shè)所有人都遵循這一條規(guī)則。同時你必須通過給舊的計數(shù)變量發(fā)送一條release消息來解除對它的所有權(quán)。(給一個nil對象發(fā)送消息在Objective-C中是沒問題的,所以即使_count并沒有被初始化過,這樣實現(xiàn)也不會有問題。)你必須在[newCount retain]之后再發(fā)送這條消息給舊對象,避免被設(shè)置的新對象和舊對象其實是同一個對象這種情況——你肯定不希望這個對象被不經(jīng)意地釋放了。
- (void)setCount:(NSNumber *)newCount { [newCount retain]; [_count release]; _count = newCount; }

在屬性值中使用訪問方法

假設(shè)你準備實現(xiàn)一個重置計數(shù)器的方法。這里有好幾種選擇。第一種實現(xiàn)是使用alloc創(chuàng)建一個新的NSNumber引用,并使用release抵消其增加的計數(shù)。
- (void)reset { NSNumber *zero = [[NSNumber alloc] initWithInteger:0]; [self setCount:zero]; [zero release]; }
第二種方法是使用一個便利構(gòu)造來創(chuàng)建一個新的NSNumber對象。此時就不再需要retain或者release消息了。
- (void)reset { NSNumber *zero = [NSNumber numberWithInteger:0]; [self setCount:zero]; }
我們發(fā)現(xiàn)這都會使用“set”方法。
下邊的代碼在通常情況下是工作正常的,但因為它傾向于避免使用訪問方法,這很可能會在某些情況下導(dǎo)致錯誤(例如,當你忘記了保持或釋放,或者,當引用變量內(nèi)存管理的含義發(fā)生變化的時候)。
- (void)reset { NSNumber *zero = [[NSNumber alloc] initWithInteger:0]; [_count release]; _count = zero; }
同時要記住,面向鍵值對編程(面向鍵值對編程是提供一種當一個對象發(fā)生改變時,其他對象能直接被通知到的一種機制,簡稱KVO)是不兼容這種修改變量的方式的。

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

唯一的你不應(yīng)該使用訪問方法來設(shè)置引用變量的地方是在初始化(一個類可能會定義多個初始化方法,從而使得它的初始化可以接受多種不同方式的輸入值,或者,提供默認初始值從而給客戶端提供更簡單的初始化方法)方法和dealloc方法中。為了將計數(shù)對象的值設(shè)置為0,你可能會使用如下代碼來實現(xiàn)init方法:
- init { self = [super init]; if (self) { _count = [[NSNumber alloc] initWithInteger:0]; } return self; }
為了將計數(shù)對象的值初始化為0以外的值,你可能會使用如下代碼來實現(xiàn)一個initWithCount:方法:
- initWithCount:(NSNumber *)startingCount { self = [super init]; if (self) { _count = [startingCount copy]; } return self; }
由于Counter類含有一個引用對象變量,所以你必須實現(xiàn)一個dealloc方法。它將給每一個引用變量發(fā)送一條release方法來解除對它們的所有權(quán),并在最后調(diào)用父類的實現(xiàn)。
- (void)dealloc { [_count release]; [super dealloc]; }

使用弱引用來避免保持循環(huán)

保持一個對象會創(chuàng)建一個對該對象的強引用。一個對象在強引用它的對象被釋放之前不會被銷毀。當兩個對象被循環(huán)引用——也就是說,它們彼此擁有對方的一個強引用(可能是直接的互相引用,也可能是由一串對象間接造成的引用),這時候,就會出現(xiàn)一種被稱作保持循環(huán)的問題。
圖1所示的對象關(guān)系圖就顯示了一種間接保持循環(huán)的情況。Document對象對文檔中的每一頁都有一個Page對象。每個Page對象又有一個屬性用來保存它們屬于哪個文檔。如果兩者對彼此都有一個強引用,那么兩者就都沒辦法被銷毀。Document對象的引用計數(shù)在Page對象釋放之前不會置0,并且Page對象在Document對象被銷毀之前不會釋放。

圖1
圖1

解決保持循環(huán)問題的方式就是使用弱引用。弱引用是一種非占有的關(guān)系,原對象含有目標對象的引用,但并不保持(retain)它。
為了保持對象圖不被破壞,我們?nèi)匀恍枰谀承┑胤绞褂脧娨茫ㄈ绻侨跻玫脑挘琍age類的對象和Paragraph的對象就會因為沒有任何所有者而被銷毀)。Cocoa建立了一種約定,一個“父”對象應(yīng)該含有“子”對象的強引用,“子”對象應(yīng)該保持對“父”對象的弱引用。
所以,在圖1中,Document對象含有對Page對象的強引用,從而保持(retain)這些對象,Page對象含有對Document對象的弱引用,從而不會保持(retain)Document對象。
在Cocoa中,弱引用的例子包括但不限于表格數(shù)據(jù)源、縮略圖組件、通知中心(給一個或多個觀察者對象發(fā)送事件消息)觀察者和混雜目標與委托(委托是一種簡單但強大的設(shè)計模式,用來代表某個對象作出行為,或和其他對象合作)。
當給那些弱引用對象發(fā)送消息時,你需要更加小心。如果給一個已經(jīng)被銷毀的對象發(fā)送了消息,你的程序?qū)罎ⅰD惚仨毭鞔_地了解對象何時是有效的。在大多數(shù)情況下,弱引用對象應(yīng)該知道其他對象是對它進行弱引用的,比如循環(huán)引用的情況,并且應(yīng)該有責任在它將要銷毀的時候通知其他對象。例如,當你給通知中心注冊了一個對象,通知中心將保存一個該對象的弱引用,并在相關(guān)的通知傳過來的時候給它發(fā)送消息。當這個對象銷毀的時候,你需要在通知中心中注銷它,從而避免通知中心還會向這個已經(jīng)不存在的對象發(fā)送消息。同樣地,當一個委托對象被銷毀的時候,你需要通過發(fā)送一個包含nil參數(shù)的setDelegate:消息給被委托對象來移除委托關(guān)系。這些消息通常在dealloc方法中被發(fā)送。

避免你正使用中的的對象被銷毀

Cocoa關(guān)于擁有權(quán)的法則指出,調(diào)用方法接收到的對象通常需要在整個作用域內(nèi)保持有效。同樣將接收到的對象從當前作用于返回的時候,也不應(yīng)擔心它會被釋放。對你的程序而言,“getter"方法返回一個緩存的引用變量或計算后的值都沒太大關(guān)系,關(guān)鍵是要在你需要使用它的時候,他應(yīng)該保持有效。
接下來是這個條例中偶發(fā)的一場,主要由兩類問題引起:

  1. 當一個對象從某種基本容器類(容器類是基礎(chǔ)框架的一種對象,它的主要作用是使用線性表、詞典或集合的方式保存一系列對象)中被移除時。
    heisenObject = [array objectAtIndex:n]; [array removeObjectAtIndex:n]; // heisenObject 現(xiàn)在可能已經(jīng)無效了
    當一個對象從這種基本容器中被移除的時候,他將會收到一條release(或者是autorelease)消息。如果這個容器對象是被移除對象的唯一所有者時,那么被移除對象(本例是heisenObject)將會在之后立刻被銷毀。
  2. 當一個“父”對象唄銷毀時。
    id parent = <#create a parent object#>; // ... heisenObject = [parent child] ; [parent release]; // 或者,例如:self.parent = nil; // heisenObject 現(xiàn)在可能已經(jīng)無效了
    在某些時候你會從其他對象那里取回一個對象,并在之后直接或間接地釋放了那個對象的父對象。當父對象被釋放從而被銷毀的時候,如果他是這個子對象的唯一擁有者,那么子對象(本例中的heisenObject)也會同時被銷毀(假設(shè)它在父對象的dealloc方法中收到的是release消息,而不是autorelease消息)。

為了防止這些情況,你應(yīng)該在接收到heisenObject對象的時候保持它,并在完成工作后釋放它。舉例如下:
heisenObject = [[array objectAtIndex:n] retain]; [array removeObjectAtIndex:n]; // 使用 heisenObject... [heisenObject release];

不要使用dealloc管理稀有資源

通常你不應(yīng)使用dealloc方法管理諸如文件描述符、網(wǎng)絡(luò)連接和緩沖區(qū)高速緩存等這些稀有資源。尤其是你不應(yīng)該在設(shè)計這些類時,在你認為應(yīng)該調(diào)用dealloc的地方調(diào)用dealloc。由于會出現(xiàn)漏洞或銷毀程序,對dealloc的調(diào)用應(yīng)該被避開或延遲。
取而代之,如果你的程序里有這種管理了稀有資源的類,你應(yīng)該在不再需要這些資源的時刻讓這些類的對象進行一些“清理”。通常你可以釋放引用,并隨后dealloc,但如果它沒有銷毀,你也不會遇到其他問題。
當你嘗試將資源管理附加在dealloc上時,經(jīng)常會導(dǎo)致問題的發(fā)生。例如:

  1. 依賴對象圖銷毀的次序。
    對象圖的銷毀是內(nèi)部無序的。即使你可能通常會想要或設(shè)置一個確切的順序,但這意味著你在增加程序的脆弱性。例如如果一個對象在不期望的時刻就被釋放或自動釋放了,那么銷毀的順序可能就會改變,這會導(dǎo)致不可預(yù)料的結(jié)果。
  2. 稀有資源不能被回收。
    內(nèi)存泄漏通常屬于可修復(fù)的程序漏洞,但它們一般不是致命的。但如果稀有資源沒有在你期望的時候釋放,這將會導(dǎo)致更加嚴重的問題。例如,如果你的應(yīng)用占用了所有的文件描述符,用戶將無法保存數(shù)據(jù)。
  3. 在錯誤的線程上調(diào)用清理邏輯。
    如果一個對象在非期望的時刻被自動釋放了,那么所有線程的自動釋放池都會釋放它。這很容易使那些只允許一個線程接觸的資源發(fā)生致命錯誤。

容器擁有他們包含的對象

當你向容器類(例如線性表、詞典或集合)的對象中添加一個對象時,容器將獲得對它的擁有權(quán)。在這個對象被移除或容器對象本身被釋放的時候,這種依賴關(guān)系會被移除。例如,如果你想創(chuàng)建一個數(shù)組:
NSMutableArray *array = <#Get a mutable array#>; NSUInteger i; // ... for (i = 0; i < 10; i++) { NSNumber *convenienceNumber = [NSNumber numberWithInteger:i]; [array addObject:convenienceNumber]; }
在這種情況下,你不需要調(diào)用alloc,所以也不需要調(diào)用release。你并不需要保持新的數(shù)字(convenienceNumber),容器類自己就會做這些。

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]; }
在這種情況下,在for循環(huán)的作用域內(nèi),你需要allocedNumber發(fā)送一條release消息來平衡alloc消息造成的引用計數(shù)增加。由于在使用addObject:方法將它添加進容器的時候,容器對象已經(jīng)保持了這個數(shù)字,所以這并不會導(dǎo)致容器中的數(shù)字被銷毀。
為了理解這一點,你可以站在開發(fā)者的角度去考慮容器類的實現(xiàn)。你得確認不會有對象在添加進來之后突然消失了,所以在它們被添加進來的時候,你給他們發(fā)送了一條retain消息。在它們被移除出去的時候,為了平衡之前的retain,你應(yīng)該再發(fā)送一條release消息。若要銷毀整個容器,那么在整個容器對象被銷毀之前,在dealloc方法里,所有容器內(nèi)的對象都應(yīng)該收到一條release消息。

所有權(quán)的法則使用保持計數(shù)實現(xiàn)

所有權(quán)的法則是使用引用計數(shù)——因為retain方法的緣故通常叫做“保持計數(shù)”,來實現(xiàn)的。每個對象都有一個保持計數(shù)。

  • 當你創(chuàng)建一個新的對象時,保持計數(shù)置1.
  • 當你給該對象發(fā)送retain消息時,保持計數(shù)+1.
  • 當你給該對象發(fā)送release消息時,保持計數(shù)-1.
    當你給該對象發(fā)送autorelease消息時,在當前自動釋放池代碼塊結(jié)束時,保持計數(shù)-1.
  • 如果一個對象的保持計數(shù)置零,它將會被銷毀。

重要提示:一般情況下沒有需要明確查詢一個對象的保持計數(shù)的原因(參考retainCount)。原因是你也許會忽略那些框架對象保持了一個你感性卻的對象,這常常會造成誤導(dǎo)。在調(diào)試內(nèi)存管理的問題時,你應(yīng)該只將精力集中在確保你的代碼符合所有權(quán)的法則。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容