iOS的內(nèi)存管理是采用引用計(jì)數(shù)的方式,引用計(jì)數(shù)分為手動(dòng)引用計(jì)數(shù)和自動(dòng)引用計(jì)數(shù)(ARC)。前者要求開發(fā)者手動(dòng)管理內(nèi)存,自己負(fù)責(zé)內(nèi)存的申請與釋放,后者是蘋果推出的自動(dòng)管理內(nèi)存的方式,但其實(shí)質(zhì)只是編譯器幫助開發(fā)者做了內(nèi)存管理的工作。理解引用計(jì)數(shù)的內(nèi)存管理機(jī)制有助于我們寫出更加內(nèi)存安全的代碼。
內(nèi)存管理/引用計(jì)數(shù)
1. 引用計(jì)數(shù)的思考方式
引用計(jì)數(shù)的思考方式遵循以下四個(gè)原則:
-
自己生成的對象,自己持有
id obj = [NSObject alloc] init]
alloc創(chuàng)建了一個(gè)對象(這里指內(nèi)存),init對此對象做初始化操作,obj持有了這個(gè)對象。同樣的,new/copy/mutablecopy也可用于生成并持有對象,除此四個(gè)關(guān)鍵字之外,以alloc/new/copy/mutablecopy開頭的方法也可用于生成并持有對象。 -
非自己生成的對象,自己也能持有
id obj = [NSMutableArray array]
obj取得了array對象,但此時(shí)并不持有它。(事實(shí)上,array方法返回的是一個(gè)autorelease對象)
[obj retain]
通過retain方法,obj便持有了array對象 。 -
不再需要自己持有的對象時(shí),釋放
持有的對象不再需要時(shí),持有者應(yīng)當(dāng)負(fù)責(zé)釋放它。
id obj = [NSObject alloc] init]
[obj release]
通過調(diào)用release方法可釋放obj持有的對象。 -
非自己持有的對象,無法釋放
id obj = [NSObject alloc] init]
[obj release]
[obj release] //程序?qū)⒈罎?/code>
當(dāng)?shù)诙握{(diào)用release時(shí),由于obj已不再持有對象,程序就會(huì)發(fā)生異常。
2. 引用計(jì)數(shù)的實(shí)現(xiàn)
-
alloc/retain/release/dealloc
alloc方法和retain方法會(huì)使對象的引用計(jì)數(shù)值加1,release方法使對象的引用計(jì)數(shù)值減1,當(dāng)對象的引用計(jì)數(shù)值為0時(shí),調(diào)用dealloc方法釋放對象。
蘋果是用散列表來管理引用計(jì)數(shù)的,鍵值為對象內(nèi)存塊地址,對應(yīng)的值保存引用計(jì)數(shù)。圖示如下

使用散列表來管理引用計(jì)數(shù)的好處是
1)散列表中存有內(nèi)存塊地址,可通過表中引用計(jì)數(shù)追溯到出問題的內(nèi)存塊地址(這在調(diào)試是很有幫助的)
2)對象內(nèi)存分配時(shí)無需再考慮引用計(jì)數(shù)所占用的內(nèi)存
-
autorelease
autorelease是自動(dòng)釋放對象的方法,它是通過NSAutoreleasePool來實(shí)現(xiàn)的。它在對象超出自身的作用域之后,調(diào)用release方法去釋放對象,具體實(shí)現(xiàn)細(xì)節(jié)為:
1)生成并持有NSAutoreleasePool對象
2)調(diào)用autorelease方法,將對象添加到NSAutoreleasePool中
3)待NSAutoreleasePool生命周期結(jié)束時(shí),所有添加到自動(dòng)釋放池中的對象均會(huì)被發(fā)送release消息來釋放自身

代碼表示如下:
NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init];
id obj = [NSObject alloc] init];
[obj autorelease];
[pool drain];
cocoa框架中,使用NSRunloop來管理NSAutoreleasePool的生成、持有和釋放,runloop開始時(shí)會(huì)創(chuàng)建自動(dòng)釋放池,睡眠和退出時(shí)會(huì)銷毀自動(dòng)釋放池,圖解如下:

很多時(shí)候,我們并不需要主動(dòng)使用NSAutoreleasePool來管理內(nèi)存,但是某些時(shí)候如果產(chǎn)生了大量的autorelease對象,而NSAutoreleasePool沒釋放前,這些對象便依舊存于內(nèi)存中,有可能會(huì)引發(fā)內(nèi)存不足的情況,此時(shí)我們可以考慮創(chuàng)建NSAutoreleasePool來及時(shí)釋放不需要的對象。當(dāng)我們創(chuàng)建了多個(gè)自動(dòng)釋放池時(shí),蘋果又是怎么管理它們的呢?答案是棧!
NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool2 = [NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool3 = [NSAutoreleasePool alloc] init];
id obj = [NSObject alloc] init];
[obj autorelease];
[pool3 drain];
[pool2 drain];
[pool drain];
很顯然,對象obj應(yīng)該會(huì)加入到pool3中,因?yàn)閜ool3是當(dāng)前正在使用的自動(dòng)釋放池。我們來看下蘋果底層相關(guān)的方法
class AutoreleasePoolPage
{
...
public:
static inline id autorelease(id obj) {} //將一個(gè)對象添加到pool中
static inline void *push() {} //將新創(chuàng)建的pool壓入棧
static inline void pop(void *token) {} //將當(dāng)前的pool出棧
...
}
ARC
1. 所有權(quán)修飾符
ARC下由編譯器幫助開發(fā)者自動(dòng)加入內(nèi)存管理代碼,因此編譯器必須知道對象何時(shí)在被持有,何時(shí)應(yīng)該被釋放,故蘋果引入了4個(gè)所有權(quán)修飾符:
__strong, __week, __unsafed_unretained, __autoreleasing
-
__strong修飾符
__strong表示對對象的強(qiáng)引用,是id類型和對象類型默認(rèn)的所有權(quán)修飾符,以下兩行代碼實(shí)質(zhì)是一樣的
id obj = [NSObject alloc] init];
id __strong obj = [NSObject alloc] init];
帶有__strong修飾符的變量在超出其作用域時(shí),即變量被廢棄時(shí),其持有的對象也隨之被釋放,代碼角度看類似這樣
{
id obj = [NSObject alloc] init];
[obj release];
}
在超出大括號(hào)作用域后,obj被廢棄,其持有的對象也因強(qiáng)引用的失效而被釋放。
-
__week修飾符
看起來__strong修飾符已經(jīng)能完美解決內(nèi)存管理的諸多問題,但事實(shí)上有一種情況是強(qiáng)引用無法解決的,即循環(huán)引用,而__week修飾符,這種弱引用方式,則可以完美解決循環(huán)引用的問題。
我們先來看一下什么是循環(huán)引用。舉個(gè)例子,比如當(dāng)A持有了B的強(qiáng)引用,B也持有了A的強(qiáng)引用,由于A和B相互持有對象的強(qiáng)引用,導(dǎo)致A和B均無法被釋放,這便是出現(xiàn)了循環(huán)引用。

有時(shí)對象引用了自身,也會(huì)發(fā)生循環(huán)引用的現(xiàn)象。

循環(huán)引用的后果是會(huì)發(fā)生內(nèi)存泄漏(不再被需要的應(yīng)該廢棄的對象卻無法被釋放),那么__week是如何解決的呢?帶有__week修飾符的變量無法持有對象的實(shí)例,換句話說,強(qiáng)引用會(huì)使對象的引用計(jì)數(shù)增加,而弱引用不會(huì)。
id __week obj = [NSObject alloc] init];
上述代碼編譯器會(huì)產(chǎn)生警告,原因是對象被生成后,由于obj持有其弱引用,導(dǎo)致對象立即被釋放。帶有__week修飾符的變量在對象被釋放后自動(dòng)變成了nil,故上述代碼最終得到的obj是nil,將代碼改為如下即可消除警告。
id __strong obj0 = [NSObject alloc] init];
id __week obj = obj0;
obj0持有對象的強(qiáng)引用,所以對象不會(huì)被釋放,obj可以正確使用對象。當(dāng)obj0超出了作用域,強(qiáng)引用失效,對象被釋放,此時(shí)obj自動(dòng)變?yōu)閚il。如此便很容易明白,當(dāng)循環(huán)引用的兩個(gè)對象相互持有對方的弱引用時(shí)(或者其中一個(gè)持有的是弱引用),并不會(huì)影響到對象的釋放,也就不再會(huì)發(fā)生內(nèi)存泄露了。
和引用計(jì)數(shù)表類似,蘋果對于week變量的管理也是通過散列表來實(shí)現(xiàn)的。將賦值對象的地址作為鍵值,由于同一對象可能被多個(gè)week變量弱引用,故同一鍵值可能對應(yīng)一組week變量。由于__week修飾的變量會(huì)占用一定的CPU資源,因此除了解決循環(huán)引用的問題,盡量避免過多的使用week變量。
-
\ __unsafe_unretained修飾符
__unsafe_unretained修飾符修飾的變量既不持有對象的強(qiáng)引用,也不持有對象的弱引用,它和__week修飾符一樣,實(shí)際上是獲得了一個(gè)指向?qū)ο蟮闹羔槪蚠_week不同的是,當(dāng)對象被釋放后,__week修飾的變量自動(dòng)變?yōu)閚il,__unsafe_unretained修飾的變量則成為了野指針!帶有__unsafe_unretained的變量不在編譯器內(nèi)存管理范圍內(nèi),編譯器是不對它做管理的,使用時(shí)應(yīng)當(dāng)謹(jǐn)慎確保其所指向的對象仍存在并未被釋放。 -
__autoreleasing修飾符
ARC下的__autoreleasing相當(dāng)于調(diào)用對象的autorelease方法,并使用@autoreleasepool來替代非ARC下的NSAutoreleasePool功能。被__autoreleasing修飾的變量所持有的對象會(huì)被加入到自動(dòng)釋放池中。

事實(shí)上,像__strong修飾符一樣,大多數(shù)時(shí)候,我們并不需要顯示對一個(gè)變量指定__autoreleasing修飾符。比如在取得非自己生成的對象引用時(shí)(使用除alloc/new/copy/mutablecopy以外的方法取得對象),變量被__week修飾符修飾時(shí),取得id或?qū)ο箢愋偷闹羔槙r(shí)(例如 id **obj),對象均會(huì)被自動(dòng)注冊到自動(dòng)釋放池中。
2. ARC規(guī)則
ARC有如下8個(gè)規(guī)則:
-
不可使用retain/release/retainCount/dealloc方法
由于ARC下編譯器會(huì)自動(dòng)在合適的位置幫助開發(fā)者插入內(nèi)存管理的代碼,因此不允許開發(fā)者再主動(dòng)調(diào)用內(nèi)存管理相關(guān)方法。 -
不可使用NSAllocateObject/NSDeallocateObject方法
事實(shí)上,alloc方法會(huì)調(diào)用NSAllocateObject來創(chuàng)建對象,并保存其引用計(jì)數(shù),單獨(dú)調(diào)用NSAllocateObject方法會(huì)對內(nèi)存管理造成混亂,因此禁止使用該方法自然也是合理的。 -
不可顯示調(diào)用dealloc方法
這里指的是開發(fā)者無需在dealloc方法中顯示調(diào)用[super dealloc],只需要做在釋放對象時(shí)一些必要的處理,比如移除之前注冊的某些觀察者。 -
使用@autoreleasepool塊代替NSAutoreleasePool
這個(gè)是顯而易見的,ARC下禁止使用NSAutoreleasePool,而采用@autoreleasepool塊代替。 -
不能使用NSZone
事實(shí)上,無論是手動(dòng)管理內(nèi)存還是使用ARC,NSZone(區(qū)域)在現(xiàn)在的運(yùn)行時(shí)系統(tǒng)中都是被忽略的。 -
對象型變量不可作為C語言結(jié)構(gòu)體成員
在C語言的規(guī)約下,結(jié)構(gòu)體成員的生命周期是無法管理的,而ARC下編譯器必須能夠正確的管理OC對象的生命周期,這顯然是矛盾的,故對象型變量不可作為C語言結(jié)構(gòu)體的成員。解決的方式有兩種,其一將對象型變量轉(zhuǎn)換為void *類型(指向不限定某一具體類型的指針),其二是用__unsafed_unretained來修飾變量,這相當(dāng)于告訴編譯器該對象不需要被編譯器管理。 -
遵循內(nèi)存管理的方法命名規(guī)則
一般而言,命名方法時(shí),謹(jǐn)慎使用以alloc/new/copy/mutablecopy開頭的方法名,這類方法應(yīng)當(dāng)返回給調(diào)用方應(yīng)該持有的對象。以init開頭的方法應(yīng)該返回實(shí)例對象。 -
顯示轉(zhuǎn)換id和void
在非ARC下,我們可以方便的直接對這兩種類型做轉(zhuǎn)換,如下所示
id obj = [NSObject alloc] init];
void * var = (void *)obj;
id obj2 = (id)var;
但是在ARC下,由于內(nèi)存管理交給了編譯器,因此編譯器需要明確每個(gè)對象的所有者,該對象是否還有所有者,當(dāng)無所有者時(shí),編譯器應(yīng)當(dāng)負(fù)責(zé)釋放該對象。因此我們在轉(zhuǎn)換類型時(shí)往往還需要考慮對象所有權(quán)問題。倘若只是想單純的賦值,那么我們可以使用__bridge修飾符來完成轉(zhuǎn)換。
id obj = [NSObject alloc] init];
void * var = (__bridge void *)obj;
id obj2 = (__bridge id)var;
與__bridge修飾符相關(guān)的兩個(gè)修飾符是__bridge_retained和__bridge_transfer,這兩個(gè)修飾符在完成轉(zhuǎn)換的同時(shí),還會(huì)對對象的所有權(quán)轉(zhuǎn)移做處理。
__bridge_retained修飾符修飾的變量在被賦值時(shí),還會(huì)獲得被賦值對象的所有權(quán),換句話說,會(huì)使對象引用計(jì)數(shù)增加。假定var是void *類型的變量,obj是id類型的變量,那么
var = (__bridge_retained void *)obj;
這等價(jià)于
var = (void *)obj;
[(id)var retain];
__bridge_transfer修飾符則正好和__bridge_retained相反,在賦值后被賦值對象隨即被釋放。同樣的,假定var是void *類型的變量,obj是id類型的變量,那么
obj = (__bridge_transfer id)var
這等價(jià)于
obj = (id)var;
[obj retain];
[(id)var release];
這種轉(zhuǎn)換常見于core Foundation對象與Foundation對象之間。前者是C語言類型對象,后者則是OC類型對象。
3. 屬性
ARC下,類屬性聲明和對應(yīng)的所有權(quán)修飾符是同樣的作用。如下所示
