理解引用計數(shù)
前言: OC 語言使用引用計數(shù)來管理內(nèi)存,也就是說, 每個對象都有個可以遞增或遞減的計數(shù)器. 如果想使某個對象繼續(xù)存活, 那就遞增其引用計數(shù); 用完了之后, 就遞減其引用計數(shù). 當(dāng)計數(shù)變?yōu)?時, 對象就會被銷毀了.
ARC實際上就是一種引用計數(shù)機制.
要點
1. 引用計數(shù)機制通過可以遞增遞減的計數(shù)器來管理內(nèi)存. 對象創(chuàng)建好之后, 其保留計數(shù)至少為1. 若保留計數(shù)為正, 則對象繼續(xù)存活. 當(dāng)保留計數(shù)降為0時, 對象就被銷毀了.
2. 在對象生命周期中, 其余對象通過引用來保留或釋放此對象. 保留與釋放操作分別會遞增或遞減保留計數(shù).
1.引用計數(shù)工作原理
在引用計數(shù)架構(gòu)下, 對象有個計數(shù)器, 用以表示當(dāng)前有多少個事物想令此對象繼續(xù)存活下去. 在OC中, 叫"保留計數(shù)"(retain count), 也可以叫"引用計數(shù)"(reference count).
NSObject協(xié)議聲明了以下三個方法用于操作計數(shù)器,以遞增或遞減其值:
- retain : 遞增保留計數(shù)
- release : 遞減保留計數(shù)
- autorelease : 待稍后清理"自動釋放池"(autorelease pool)時, 再遞減保留計數(shù).
查看保留計數(shù)的方法: retainCount, 這方法不太管用,在調(diào)試時也是如此,蘋果公司并不建議使用
對象創(chuàng)建出來時,其保留計數(shù)至少為1. 若想令對象繼續(xù)存活,則調(diào)用retain方法. 若不再使用此對象, 就調(diào)用release或autorelease方法. 當(dāng)保留計數(shù)為0時,對象就被回收了, 系統(tǒng)會將其占用的內(nèi)存標(biāo)記為"可重用". 此時,所有指向該對象的引用就無效了.
在對象生命周期中, 其保留計數(shù)時而遞增, 時而遞減, 最終歸零
應(yīng)用程序在其生命周期中會創(chuàng)建很多對象, 這些對象都相互聯(lián)系著. 例如下圖中: 對象B與對象C 都引用了對象A. 若對象B與對象C都不再使用對象A, 則其保留計數(shù)減為0, 于是對象A被銷毀了.
對象圖里所有指向?qū)ο驛的引用均釋放之后, 對象A所占內(nèi)存亦可回收
圖中還有其他對象引用對象B與對象C, 而應(yīng)用程序里又有另外一些對象引用其他對象. 如果按"引用樹"回溯,最終會發(fā)現(xiàn)一個"根對象". 在iOS應(yīng)用程序中, 是UIApplication對象. Max OS中則是NSApplication對象,兩者都是應(yīng)用程序啟動時創(chuàng)建的單例.
下面這段代碼有助于理解這些方法的用法:
NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:2019];
[array addObject:number];
[number release];
// do something with 'array'
[array release];
在ARC下, 無法編譯release方法; 在OC中, 調(diào)用alloc方法所返回的對象由調(diào)用者擁有. 對象引用計數(shù)加1, 注意: 這并不是說對象此時的保留計數(shù)必定是1
. 在alloc 或 initWithInt: 方法實現(xiàn)代碼中, 也許還有其他對象引用了此對象, 所以其保留計數(shù)可能會大于1. 能夠肯定的是: 其保留計數(shù)至少為1
. 保留計數(shù)的概念應(yīng)該這樣理解, 絕不應(yīng)該說保留計數(shù)一定是某個值, 只能說你執(zhí)行的操作遞增或遞減了該計數(shù)
.
創(chuàng)建完數(shù)組后, 把number對象加入其中, 調(diào)用數(shù)組的 addObject: 方法時, 數(shù)組也會在 number上調(diào)用 retain 方法, 來繼續(xù)保留該對象. 這時計數(shù)至少為2. 接著釋放 number 對象, 保留的計數(shù)至少為1, 調(diào)用 release 之后, 已經(jīng)無法保證所指的number 對象是否存活, 也就不能正常使用 number變量了. 當(dāng)前本例中,數(shù)組還引用著number對象, 在調(diào)用release之后依然存活, 然而絕不應(yīng)假設(shè)此對象一定存活, 也就是說不能像下面那樣編碼:
[number release];
NSLog(@"number= %@", number);
如果由于某些原因, 其引用計數(shù)減為0, 那么number對象所占內(nèi)存也許會被回收, 此時調(diào)用 NSLog 可能會使程序崩潰!!!
為避免不經(jīng)意間使用了無效對象, 一般用完release 之后都會清空指針. 保證不會出現(xiàn)可能指向無效對象的指針, 這種指針通常稱為"懸掛指針".
可以照下面編碼來防止此情況發(fā)生:
[number release];
number = nil;
2.屬性存取方法中的內(nèi)存管理
訪問屬性時, 會用到相關(guān)實例變量的獲取方法及設(shè)置方法. 若屬性為"strong"關(guān)系, 則設(shè)置的屬性值會保留.
例: 有個名叫 foo 的屬性由名為 _foo 的實例屬性所實現(xiàn), 那么,該屬性設(shè)置方法會是這樣的:
- (void)setFoo:(id)foo {
[foo retain]; // 保留新值
[_foo release]; // 釋放舊值
_foo = foo;
}
此方法將保留新值并釋放舊值, 然后更新實例變量, 令其指向新值. 順序很重要, 假如還未保留新值就先把舊值釋放了, 而且兩個值又指向同一個對象, 那么, 先執(zhí)行release操作就可能導(dǎo)致系統(tǒng)將此對象永久回收. 而后續(xù)的retain操作無法令這個已經(jīng)回收的對象復(fù)生, 于是實例變量也就成了懸掛指針.
3. 自動釋放池
在OC的引用計數(shù)架構(gòu)中, 自動釋放池是一個重要特性. 調(diào)用release 會立刻遞減對象的保留計數(shù)(而且可能令系統(tǒng)回收此對象), 有時候可以改為調(diào)用 autorelease, 此方法會在稍后遞減計數(shù), 通常是在下一次"事件循環(huán)"時遞減, 不過也可能執(zhí)行更早些.
此特性很有用, 尤其在方法中返回對象時; 以下面方法為例:
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@",self];
return str;
}
此時返回的str對象其保留計數(shù)比期望值多1, 因為調(diào)用alloc會令保留計數(shù)加1, 而又沒有與之對應(yīng)的釋放操作. 有retain,就應(yīng)該有對應(yīng)的release, 將其抵消. 這并不是說保留計數(shù)就一定是1, 可能大于1(這取決于initWithFormat:方法內(nèi)部實現(xiàn)).
但是,不能在方法內(nèi)釋放str,否則還沒等方法返回, 系統(tǒng)就把該對象回收了. 這里應(yīng)該用autorelease, 它會在稍后釋放對象, 從而給調(diào)用者留下足夠長時間在其需要時先保留返回值. 也可以這樣理解: 此方法可以保證對象在跨越"方法調(diào)用邊界"后仍然存活.
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@",self];
return [str autorelease];
}
修改之后,stringValue方法把NSString對象返回給調(diào)用者時,此對象必然存活. 所以能像下面使用:
NSString *str = [self stringValue];
NSLog(@"the string is %@",str);
由于返回的str對象將于稍后自動釋放,多出來的那一次retain操作到時自然就會抵消,無需再執(zhí)行內(nèi)存管理操作. 因為自動釋放池中的釋放操作要等到下一次事件循環(huán)時才會執(zhí)行.
autorelease能延長對象生命期,使其在跨越方法調(diào)用邊界后依然可以存活一段時間.
4. 循環(huán)引用
使用引用計數(shù)機制時, 經(jīng)常要注意一個問題就是: 循環(huán)引用; 也就是呈環(huán)狀相互引用的多個對象. 這將導(dǎo)致內(nèi)存泄漏, 因為循環(huán)中的對象其保留計數(shù)不會降為0.
通常采用"弱引用"來解決此問題, 或是從外界命令循環(huán)中的某個對象不再保留另外一個對象. 這兩種方法都能打破保留環(huán), 避免內(nèi)存泄漏.