讓我們從後面開始,當垃圾收集被關掉時對象銷毀的方式。在此背景下Cocoa和Objective-C 選擇一個自動的,策略驅動的過程來保持對象的存在並在不再被需要的時候銷毀它們。
這個過程和策略依賴於引用計數的概念。每個Cocoa對象攜帶一個整數用來指示對其存在感興趣的其它對象的數目。這個整數被稱為對象的保留數(retain count)(“retain”用來避免和術語“reference”重疊)。 當你建立一個對象時,或者通過一個類Factory 方法或者使用alloc 或allocWithZone: 類方法, Cocoa 做了一些很重要的事情:
它設定對象的isa 指標- NSObject 類的唯一公用成員變數-以指向這個對象的類,這樣把這個對象整合到運行時視圖類層次。(參見對象建立“Object Creation”擷取更多資訊)
它設定對象的保留數(retain count)- 一種由運行時管理的隱藏的成員變數- 為1。(這裡假設一個對象的建立者對其存在感興趣)
在對象分配後,你一般會設定它的成員變數為一個合理的初始值。 (NSObject 聲明init 方法作為這個目的的原形)。 這個對象現在已經可以使用了;你可以發送訊息給它,把它傳遞給其他對象,等等。
注意: 因為一個初始化器可以返回一個不是顯式聲明的那個對象,慣例是嵌套alloc 訊息運算式在init 訊息裡(或者其他初始化器)- 比如:
- <code>id anObj = [[MyClass alloc] init];</code>
當你釋放一個對象- 也就是,發送一個release 訊息給它 – NSObject 減少其保留數。如果這個保留數從1變成0,這個對象會被釋放。釋放分成兩個步驟。首先,對象的dealloc 方法被調用來釋放成員變數並動態釋放分配的記憶體。然後作業系統銷毀對象自身並回收該對象曾經佔用的記憶體。
重要: 你永遠不該直接調用一個對象的dealloc 方法。
要是你不想一個對象馬上消失?如果你在從別處接收到一個對象時給它發送了一個retain 訊息,這個對象的保留數(retain count)被增加為2。現在在釋放之前需要兩個release 訊息。圖2-4 圖示了這個相對簡化的情境。
Figure 2-4 一個對象的生命週期- 簡化視圖
當然,在這個情境中,一個對象的建立者不需要保留這個對象。它早就擁有了這個對象。但是如果這個建立者在一個訊息中傳遞這個對象給另外的對象,情況就發生了變化。在一個Objective-C 程式中,一個接收一些其他對象的對象總是假設在其獲得的範圍內有效。這個接收對象可以發送訊息給被接受的對象以及傳遞給其他對象。這個假設需要發送對象運轉並且不會過早的釋放這個對象,當一個客戶對象有一個指向它的引用時。
如果客戶對象想在接收到的對象程式存取範圍之外保留它,可以retain 它- 也就是,發送一個retain 訊息給它。保留一個對象增加其保留計數,並由此表達該對象的一個所有權。這個客戶對象假設稍後釋放該對象的一個職責。如果一個對象的建立者釋放它,但是一個客戶對象保留了這個相同的對象,這個對象保持存在直到這個客戶釋放了它。圖2-5 說明了這個順序:
Figure 2-5 保留一個接收到的對象
和保留一個對象相反,你可以通過給它發送一個copy 或copyWithZone:訊息來拷貝它。(很多子類,如果不是大多數,封裝了一些採用或符合這個協議的資料)。拷貝一個對象不僅複製它而且常常總是重設它的保留計數為1(參見圖2-6)。拷貝可以是淺拷貝也可以是深拷貝,這依賴於這個對象的本質以及它的預期用途。一個深拷貝複製出一個可以承擔成員變數相同作用的對象,而淺拷貝僅僅增加這些成員變數的引用。
談到使用,區別一個copy 和retain 的是前者聲稱這個對象的單獨使用權;新的擁有者可以改變這個拷貝對象而無須關心它的原始對象。一般而言你拷貝一個對象而不是保留它,當它是一個數值對象- 也就是,一個對象封裝了一些基本資料(如整數)。特別是這個對象本身是可變的,比如一個NSMutableString,對於非可變對象,copy和retain 可以等同並且也許可以用類似方法來實現。
Figure 2-6 拷貝一個接收到的對象
你 也許注意到了這個機制關於管理對象生命週期的一個潛在的問題。建立了一個對象並傳遞給另外的對象的這個建立者對象並不總是知道什麼時候可以安全的釋放掉這 個被建立出來的對象。有可能在堆棧中有這個對象的多個引用,有一些是建立者對象所不知道的。如果這個建立者對象釋放掉這個被建立的對象然後其他對象給這個 已銷毀對象發送訊息的話,程式將崩潰。為了消除這個問題,Cocoa 引入了一個延遲釋放的機制叫做autoreleasing。
Autoreleasing 使用自釋放池(autorelease pools) (以NSAutoreleasePool 類定義)。一個自釋放池是一個明確定義了範圍的對象集合,這個範圍標記著最終什麼時候釋放。自釋放池可以被嵌套。當你發送一個 autorelease 訊息, 一個該對象的引用被放進最近的自釋放池中。它仍然是一個有效對象,所以其他在自釋放池定義範圍內的對象可以給它發送訊息。當程式執行到範圍末尾時,這個池被釋放,而且,相應的,池中的所有對象也將被釋放(參見圖2-7)。如果你在開發一個應用程式你可能不需要建立一個自釋放池,因為應用程式工具箱(Application Kit)會自動建立一個範圍為應用程式事件周期的自釋放池.
Figure 2-7 一個自釋放池
iPhone OS 提示: 因為在iPhone OS 中,應用程式在一個更加記憶體受限的環境中運行,所以不鼓勵在應用程式建立很多個物件的方法或程式碼片段中使用自釋放池(比如,迴圈)。相反,你應該在任何可能的時候顯式的釋放對象。
到目前為止關於對象生命週期的討論集中在貫穿周期的對象管理機制上。但是一個對象擁有者策略指導如何使用這些機制。這個策略可以總結如下:
如果你通過分配並初始化來建立( create )一個對象(比如 [[MyClass alloc] init]),你將擁有這個對象並負責釋放它。這個規則同樣適用於使用NSObject 簡便方法(convenient method)new。
如果你拷貝(copy)一個對象,你將擁有這個拷貝的對象並負責釋放它。
如果你保留( retain )一個對象,你擁有該對象部分的所有權並且當你不需要的時候釋放它。
相反的,如果你從其他一些對象接收一個對象,你不擁有這個對象並且不應該釋放它。(這個規則有一些少數的例外,已在參考文檔中顯式的標註)
和任何規則集一樣,有一些例外和已知問題(“gotchas”):
如果你通過類Factory 方法建立了一個對象(比如NSMutableArray arrayWithCapacity: 方法),假設你接收的這個對象是自動釋放的。你不應該自己釋放這個對象而且如果你想保持其存在的話應該retain它。
為了避免循環參考,一個子物件永遠不該retain它的父物件。(一個父物件是這個子物件的建立者或者一個以成員變數包含該這個子物件的對象。)
注意: 上面指南中的“Release”意味著發送一個release 訊息或者一個autorelease 訊息給一個對象。
如果你不遵循這個所有權策略,在你的應用程式中很可能會發生兩件糟糕的事情。因為你沒有釋放建立,拷貝,或者保留的對象,你的應用程式將存在記憶體流失。或者當你給一個已從其他地方釋放的對象發送訊息時導致你的程式崩潰。這裡還有一個警告:調試這些問題費時費力。
一個另外的可能發生在一個對象生命週期裡的基本事件是歸檔(archiving)。歸檔把組成一個物件導向的程式的互相關聯的對象網路-對象圖-轉換成一個持久格式(通常是一個檔案),儲存了標識和每個圖中對象的關係。當程式被解歸檔時,它的對象圖從歸檔中重新構建。為了參與歸檔(和解歸檔),一個對象必須能夠編碼(和解碼)。它的成員變數使用NSCoder 類方法。 NSObject 採用NSCoding 協議來完成這個目的。