iOS開(kāi)發(fā)指南 內(nèi)存管理工作原理
讓我們從后面開(kāi)始,當(dāng)垃圾收集被關(guān)掉時(shí)對(duì)象銷(xiāo)毀的方式。在此背景下Cocoa和Objective-C 選擇一個(gè)自動(dòng)的,策略驅(qū)動(dòng)的過(guò)程來(lái)保持對(duì)象的存在并在不再被需要的時(shí)候銷(xiāo)毀它們。
這個(gè)過(guò)程和策略依賴(lài)于引用計(jì)數(shù)的概念。每個(gè)Cocoa對(duì)象攜帶一個(gè)整數(shù)用來(lái)指示對(duì)其存在感興趣的其它對(duì)象的數(shù)目。這個(gè)整數(shù)被稱(chēng)為對(duì)象的保留數(shù)(retain count)(“retain”用來(lái)避免和術(shù)語(yǔ)“reference”重疊)。 當(dāng)你創(chuàng)建一個(gè)對(duì)象時(shí),或者通過(guò)一個(gè)類(lèi)工廠方法或者使用alloc 或allocWithZone: 類(lèi)方法, Cocoa 做了一些很重要的事情:
它設(shè)置對(duì)象的isa 指針- NSObject 類(lèi)的***公共成員變量-以指向這個(gè)對(duì)象的類(lèi),這樣把這個(gè)對(duì)象集成到運(yùn)行時(shí)視圖類(lèi)層次。(參見(jiàn)對(duì)象創(chuàng)建“Object Creation”獲取更多信息)
它設(shè)置對(duì)象的保留數(shù)(retain count)- 一種由運(yùn)行時(shí)管理的隱藏的成員變量- 為1。(這里假設(shè)一個(gè)對(duì)象的創(chuàng)建者對(duì)其存在感興趣)
在對(duì)象分配后,你一般會(huì)設(shè)置它的成員變量為一個(gè)合理的初始值。 (NSObject 聲明init 方法作為這個(gè)目的的原形)。 這個(gè)對(duì)象現(xiàn)在已經(jīng)可以使用了;你可以發(fā)送消息給它,把它傳遞給其他對(duì)象,等等。
注意: 因?yàn)橐粋€(gè)初始化器可以返回一個(gè)不是顯式聲明的那個(gè)對(duì)象,慣例是嵌套alloc 消息表達(dá)式在init 消息里(或者其他初始化器)- 比如:
- <code>id anObj = [[MyClass alloc] init];</code>
當(dāng)你釋放一個(gè)對(duì)象- 也就是,發(fā)送一個(gè)release 消息給它 – NSObject 減少其保留數(shù)。如果這個(gè)保留數(shù)從1變成0,這個(gè)對(duì)象會(huì)被釋放。釋放分成兩個(gè)步驟。首先,對(duì)象的dealloc 方法被調(diào)用來(lái)釋放成員變量并動(dòng)態(tài)釋放分配的內(nèi)存。然后操作系統(tǒng)銷(xiāo)毀對(duì)象自身并回收該對(duì)象曾經(jīng)占用的內(nèi)存。
重要: 你永遠(yuǎn)不該直接調(diào)用一個(gè)對(duì)象的dealloc 方法。
要是你不想一個(gè)對(duì)象馬上消失?如果你在從別處接收到一個(gè)對(duì)象時(shí)給它發(fā)送了一個(gè)retain 消息,這個(gè)對(duì)象的保留數(shù)(retain count)被增加為2?,F(xiàn)在在釋放之前需要兩個(gè)release 消息。圖2-4 圖示了這個(gè)相對(duì)簡(jiǎn)化的場(chǎng)景。
Figure 2-4 一個(gè)對(duì)象的生命周期- 簡(jiǎn)化視圖

當(dāng)然,在這個(gè)場(chǎng)景中,一個(gè)對(duì)象的創(chuàng)建者不需要保留這個(gè)對(duì)象。它早就擁有了這個(gè)對(duì)象。但是如果這個(gè)創(chuàng)建者在一個(gè)消息中傳遞這個(gè)對(duì)象給另外的對(duì)象,情況就發(fā)生了變化。在一個(gè)Objective-C 程序中,一個(gè)接收一些其他對(duì)象的對(duì)象總是假設(shè)在其獲得的范圍內(nèi)有效。這個(gè)接收對(duì)象可以發(fā)送消息給被接受的對(duì)象以及傳遞給其他對(duì)象。這個(gè)假設(shè)需要發(fā)送對(duì)象運(yùn)轉(zhuǎn)并且不會(huì)過(guò)早的釋放這個(gè)對(duì)象,當(dāng)一個(gè)客戶(hù)對(duì)象有一個(gè)指向它的引用時(shí)。
如果客戶(hù)對(duì)象想在接收到的對(duì)象程序訪問(wèn)范圍之外保留它,可以retain 它- 也就是,發(fā)送一個(gè)retain 消息給它。保留一個(gè)對(duì)象增加其保留計(jì)數(shù),并由此表達(dá)該對(duì)象的一個(gè)所有權(quán)。這個(gè)客戶(hù)對(duì)象假設(shè)稍后釋放該對(duì)象的一個(gè)職責(zé)。如果一個(gè)對(duì)象的創(chuàng)建者釋放它,但是一個(gè)客戶(hù)對(duì)象保留了這個(gè)相同的對(duì)象,這個(gè)對(duì)象保持存在直到這個(gè)客戶(hù)釋放了它。圖2-5 說(shuō)明了這個(gè)順序:
Figure 2-5 保留一個(gè)接收到的對(duì)象

和保留一個(gè)對(duì)象相反,你可以通過(guò)給它發(fā)送一個(gè)copy 或copyWithZone:消息來(lái)拷貝它。(很多子類(lèi),如果不是大多數(shù),封裝了一些采用或符合這個(gè)協(xié)議的數(shù)據(jù))??截愐粋€(gè)對(duì)象不僅復(fù)制它而且常??偸侵刂盟谋A粲?jì)數(shù)為1(參見(jiàn)圖2-6)??截惪梢允菧\拷貝也可以是深拷貝,這依賴(lài)于這個(gè)對(duì)象的本質(zhì)以及它的預(yù)期用途。一個(gè)深拷貝復(fù)制出一個(gè)可以承擔(dān)成員變量相同作用的對(duì)象,而淺拷貝僅僅增加這些成員變量的引用。
談到使用,區(qū)別一個(gè)copy 和retain 的是前者聲稱(chēng)這個(gè)對(duì)象的單獨(dú)使用權(quán);新的擁有者可以改變這個(gè)拷貝對(duì)象而無(wú)須關(guān)心它的原始對(duì)象。一般而言你拷貝一個(gè)對(duì)象而不是保留它,當(dāng)它是一個(gè)數(shù)值對(duì)象- 也就是,一個(gè)對(duì)象封裝了一些基本數(shù)據(jù)(如整數(shù))。特別是這個(gè)對(duì)象本身是可變的,比如一個(gè)NSMutableString,對(duì)于非可變對(duì)象,copy和retain 可以等同并且也許可以用類(lèi)似方法來(lái)實(shí)現(xiàn)。
Figure 2-6 拷貝一個(gè)接收到的對(duì)象

你 也許注意到了這個(gè)機(jī)制關(guān)于管理對(duì)象生命周期的一個(gè)潛在的問(wèn)題。創(chuàng)建了一個(gè)對(duì)象并傳遞給另外的對(duì)象的這個(gè)創(chuàng)建者對(duì)象并不總是知道什么時(shí)候可以安全的釋放掉這 個(gè)被創(chuàng)建出來(lái)的對(duì)象。有可能在堆棧中有這個(gè)對(duì)象的多個(gè)引用,有一些是創(chuàng)建者對(duì)象所不知道的。如果這個(gè)創(chuàng)建者對(duì)象釋放掉這個(gè)被創(chuàng)建的對(duì)象然后其他對(duì)象給這個(gè) 已銷(xiāo)毀對(duì)象發(fā)送消息的話(huà),程序?qū)⒈罎?。為了消除這個(gè)問(wèn)題,Cocoa 引入了一個(gè)延遲釋放的機(jī)制叫做autoreleasing。
Autoreleasing 使用自釋放池(autorelease pools) (以NSAutoreleasePool 類(lèi)定義)。一個(gè)自釋放池是一個(gè)明確定義了范圍的對(duì)象集合,這個(gè)范圍標(biāo)記著最終什么時(shí)候釋放。自釋放池可以被嵌套。當(dāng)你發(fā)送一個(gè) autorelease 消息, 一個(gè)該對(duì)象的引用被放進(jìn)最近的自釋放池中。它仍然是一個(gè)有效的對(duì)象,所以其他在自釋放池定義范圍內(nèi)的對(duì)象可以給它發(fā)送消息。當(dāng)程序執(zhí)行到范圍末尾時(shí),這個(gè)池被釋放,而且,相應(yīng)的,池中的所有對(duì)象也將被釋放(參見(jiàn)圖2-7)。如果你在開(kāi)發(fā)一個(gè)應(yīng)用程序你可能不需要建立一個(gè)自釋放池,因?yàn)閼?yīng)用程序工具箱(Application Kit)會(huì)自動(dòng)建立一個(gè)范圍為應(yīng)用程序事件周期的自釋放池.
Figure 2-7 一個(gè)自釋放池

iPhone OS 提示: 因?yàn)樵趇Phone OS 中,應(yīng)用程序在一個(gè)更加內(nèi)存受限的環(huán)境中運(yùn)行,所以不鼓勵(lì)在應(yīng)用程序創(chuàng)建很多對(duì)象的方法或代碼段中使用自釋放池(比如,循環(huán))。相反,你應(yīng)該在任何可能的時(shí)候顯式的釋放對(duì)象。
到目前為止關(guān)于對(duì)象生命周期的討論集中在貫穿周期的對(duì)象管理機(jī)制上。但是一個(gè)對(duì)象擁有者策略指導(dǎo)如何使用這些機(jī)制。這個(gè)策略可以總結(jié)如下:
如果你通過(guò)分配并初始化來(lái)創(chuàng)建( create )一個(gè)對(duì)象(比如 [[MyClass alloc] init]),你將擁有這個(gè)對(duì)象并負(fù)責(zé)釋放它。這個(gè)規(guī)則同樣適用于使用NSObject 簡(jiǎn)便方法(convenient method)new。
如果你拷貝(copy)一個(gè)對(duì)象,你將擁有這個(gè)拷貝的對(duì)象并負(fù)責(zé)釋放它。
如果你保留( retain )一個(gè)對(duì)象,你擁有該對(duì)象部分的所有權(quán)并且當(dāng)你不需要的時(shí)候釋放它。
相反的,如果你從其他一些對(duì)象接收一個(gè)對(duì)象,你不擁有這個(gè)對(duì)象并且不應(yīng)該釋放它。(這個(gè)規(guī)則有一些少數(shù)的例外,已在參考文檔中顯式的標(biāo)注)
和任何規(guī)則集一樣,有一些例外和已知問(wèn)題(“gotchas”):
如果你通過(guò)類(lèi)工廠方法創(chuàng)建了一個(gè)對(duì)象(比如NSMutableArray arrayWithCapacity: 方法),假設(shè)你接收的這個(gè)對(duì)象是自動(dòng)釋放的。你不應(yīng)該自己釋放這個(gè)對(duì)象而且如果你想保持其存在的話(huà)應(yīng)該retain它。
為了避免循環(huán)引用,一個(gè)子對(duì)象永遠(yuǎn)不該retain它的父對(duì)象。(一個(gè)父對(duì)象是這個(gè)子對(duì)象的創(chuàng)建者或者一個(gè)以成員變量包含該這個(gè)子對(duì)象的對(duì)象。)
注意: 上面指南中的“Release”意味著發(fā)送一個(gè)release 消息或者一個(gè)autorelease 消息給一個(gè)對(duì)象。
如果你不遵循這個(gè)所有權(quán)策略,在你的應(yīng)用程序中很可能會(huì)發(fā)生兩件糟糕的事情。因?yàn)槟銢](méi)有釋放創(chuàng)建,拷貝,或者保留的對(duì)象,你的應(yīng)用程序?qū)⒋嬖趦?nèi)存泄漏?;蛘弋?dāng)你給一個(gè)已從其他地方釋放的對(duì)象發(fā)送消息時(shí)導(dǎo)致你的程序崩潰。這里還有一個(gè)警告:調(diào)試這些問(wèn)題費(fèi)時(shí)費(fèi)力。
一個(gè)另外的可能發(fā)生在一個(gè)對(duì)象生命周期里的基本事件是歸檔(archiving)。歸檔把組成一個(gè)面向?qū)ο蟮某绦虻幕ハ嚓P(guān)聯(lián)的對(duì)象網(wǎng)絡(luò)-對(duì)象圖-轉(zhuǎn)換成一個(gè)持久格式(通常是一個(gè)文件),保存了標(biāo)識(shí)和每個(gè)圖中對(duì)象的關(guān)系。當(dāng)程序被解歸檔時(shí),它的對(duì)象圖從歸檔中重新構(gòu)建。為了參與歸檔(和解歸檔),一個(gè)對(duì)象必須能夠編碼(和解碼)。它的成員變量使用NSCoder 類(lèi)方法。 NSObject 采用NSCoding 協(xié)議來(lái)完成這個(gè)目的。更多關(guān)于對(duì)象歸檔的內(nèi)容,請(qǐng)參見(jiàn)對(duì)象歸檔(“Object Archives”)。