第5章 內(nèi)存管理
第29條:理解引用計數(shù)
Objective-C 語言使用引用計數(shù)來管理內(nèi)存,每個對象都有一個可以遞增或遞減的計數(shù)器蝇恶。
-
使用 ARC 時拳魁,所有與引用計數(shù)有關(guān)的方法都無法編譯。
- (instancetype)retain OBJC_ARC_UNAVAILABLE; - (oneway void)release OBJC_ARC_UNAVAILABLE; - (instancetype)autorelease OBJC_ARC_UNAVAILABLE; - (NSUInteger)retainCount OBJC_ARC_UNAVAILABLE; - (struct _NSZone *)zone OBJC_ARC_UNAVAILABLE;
引用計數(shù)工作原理
- 對象有個計數(shù)器撮弧,表示當前有多少個事物想令此對象繼續(xù)存活潘懊。這在 Objective-C 中叫做 保留計數(shù)(retain count)或者 引用計數(shù)(reference count)。
- 對象創(chuàng)建出來時贿衍,其引用計數(shù)至少為1授舟。
- 不應(yīng)該說引用計數(shù)一定是某個值,而應(yīng)該說執(zhí)行的操作遞增/遞減了該計數(shù)贸辈。
- 在 iOS 應(yīng)用程序中释树,根對象為 UIApplication 單例對象。
NSObject 協(xié)議聲明了三個方法用于操作計數(shù)器:
-
retain
:遞增引用計數(shù)。 -
release
: 遞減引用計數(shù)奢啥。 -
autorelease
: 待稍后清理“自動釋放池”(autorelease pool)時秸仙,再遞減引用計數(shù)。
NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:1337]; // retainCount >= 1
[array addObject:number]; // retainCount >= 2
[number release]; // retainCount >= 1
number = nil; // 避免懸掛指針(指向無效對象的指針)
// do somethind with "array"
[array release];
屬性存取方法中的內(nèi)存管理
// strong 屬性:
- (void)setFoo:(id)foo {
// 順序很重要桩盲,如果先 release寂纪,則此對象將被永久回收
[foo retain]; // 先保留新值。
[_foo release]; // 再釋放舊值赌结。
_foo = foo; // 更新實例變量捞蛋,令其指向新值。
}
自動釋放池
-
autorelease
方法會在稍后遞減計數(shù)柬姚,通常是在下一次 事件循環(huán)(event loop)時遞減拟杉。 -
autorelease
能延長對象的生命周期,保證對象在 跨越方法調(diào)用邊界 后依然可以存活一段時間伤靠。
示例:
- (NSString *)stringValue {
NSString *str = [[NSString alloc]
initWithFormat:@"I am this:%@",self]; // retainCount >= 1
return str; // retainCount >= 2
}
此處需要使用 autorelease
釋放對象:
- (NSString *)stringValue {
NSString *str = [[NSString alloc]
initWithFormat:@"I am this:%@",self];
return [str autorelease];
}
引用循環(huán)/保留環(huán)
引用循環(huán):呈環(huán)狀相互引用的多個對象捣域,內(nèi)存無法正常釋放,導(dǎo)致內(nèi)存泄漏宴合。
解決方法:弱引用(weak reference)焕梅、從外界命令循環(huán)中的某個對象不再保留另外一個對象。
要點
- 引用計數(shù)機制通過可以遞增遞減的計數(shù)器來管理內(nèi)存卦洽。對象創(chuàng)建好后贞言,其保留計數(shù)至少為1。若保留計數(shù)為正阀蒂,則對象繼續(xù)存活该窗。當保留計數(shù)降為0時,對象就被銷毀了蚤霞。
- 在對象生命期中酗失,其余對象通過引用來保留或釋放此對象。保留與釋放操作分別會遞增及遞減保留計數(shù)昧绣。
第30條:以 ARC 簡化引用計數(shù)
- ARC 原理:靜態(tài)分析器(static analyzer)可以查明內(nèi)存管理問題规肴,也可以預(yù)先加入適當?shù)谋A艋蜥尫挪僮饕员苊鈨?nèi)存管理問題。
- 在 ARC 中夜畴,不能直接調(diào)用
retain
拖刃、release
、autorelease
贪绘、dealloc
方法兑牡。 - ARC 會直接調(diào)用底層 C 語言函數(shù)自動管理內(nèi)存。
- ARC 會自動調(diào)用“保留”與“釋放”方法税灌。
- ARC 包含運行期組件均函。
使用 ARC 時必須遵循的方法命名規(guī)則
若方法名以下列詞語開頭亿虽,則其返回的對象歸調(diào)用者所有:
- alloc
- new
- copy
- mutableCopy
歸調(diào)用者所有:【調(diào)用上述四種方法的那段代碼】要負責(zé)【釋放方法所返回的對象】。
變量的內(nèi)存管理語義
默認情況下边酒,每個變量都是指向?qū)ο蟮膹娨谩?/p>
修飾符:
-
__strong
:默認語義经柴,保留此值。 -
__unsafe_unretained
:不保留此值墩朦,這么做可能不安全坯认,因為等到再次使用變量時,其對象可能已經(jīng)回收了氓涣。 -
__weak
:不保留此值牛哺,但是變量可以安全使用,因為如果系統(tǒng)把這個對象回收了劳吠,那么變量也會自動清空引润。 -
__autoreleasing
:把對象“按引用傳遞”給方法時,使用這個特殊的修飾符痒玩。此值在方法返回時自動釋放淳附。
ARC 如何清理實例變量
ARC 會借用 Objective-C++ 的一項特性來生成清理例程。
ARC 會自動生成回收對象時所執(zhí)行的代碼蠢古。
-
CoreFoundation 對象不歸 ARC 管理奴曙,開發(fā)者必須適時調(diào)用
CFRetain/CFRelease
。- (void)dealloc { CFRelease(coreFoundationObject); free(_heapAllocatedMemoryBlob); }
覆寫內(nèi)存管理方法
在 ARC 環(huán)境下不要覆寫 release
方法草讶。
要點
- 有 ARC 之后洽糟,程序員就無須擔(dān)心內(nèi)存管理問題了。使用 ARC 來編程堕战,可省去類中的許多 "樣板代碼"坤溃。
- ARC 管理對象生命期的辦法基本上就是:在合適的地方插入"保留"及"釋放"操作。在 ARC 環(huán)境下嘱丢,變量的內(nèi)存管理語義可以通過修飾符指明薪介,而原來則需要手工執(zhí)行"保留"及"釋放"操作。
- 由方法所返回的對象越驻,其內(nèi)存管理語義總是通過方法名來體現(xiàn)昭灵。ARC 將此確定為開發(fā)者必須遵守的規(guī)則。
- ARC 只負責(zé)管理 Objective-C 對象的內(nèi)存伐谈。尤其要注意:CoreFoundation 對象不歸 ARC 管理,開發(fā)者必須適時調(diào)用
CFRetain/CFRelease
试疙。
第31條:在 dealloc 方法中只釋放引用并解除監(jiān)聽
-
dealloc
方法主要用于釋放對象所擁有的引用(即釋放所有 Objective-C 對象)诵棵,ARC 會通過自動生成的 .cxx_destruct 方法在dealloc
中自動添加釋放代碼。 - 非 Objective-C 對象(如 CoreFoundation 對象)則必須手工釋放祝旷。
- 還需要清理觀測行為(observation behavior)履澳,注銷通知嘶窄。
- 開銷較大或系統(tǒng)內(nèi)稀缺資源不應(yīng)在
dealloc
中釋放,當應(yīng)用程序用完資源后應(yīng)及時釋放距贷,還有一個原因是:系統(tǒng)并不保證每個創(chuàng)建出來的對象的dealloc
都會執(zhí)行柄冲。
- (void)dealloc {
CFRelease(coreFoundationObject);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-
如果對象管理著某些資源,那么在
dealloc
方法中也要調(diào)用“清理方法”忠蝗,以防開發(fā)者忘記清理這些資源现横。- (void)close { /** clean up resoureces */ _close = YES; } - (void)dealloc { if (!_close) { NSLog(@"ERROR:close was not called before dealloc"); [self close]; } }
要點
- 在
dealloc
方法里,應(yīng)該做的事情就是釋放指向其它對象的引用阁最,并取消原來訂閱的"鍵值觀測"(KVO)或 NSNotificationCenter 等通知戒祠,不要做其他事情。 - 如果對象持有文件描述符等系統(tǒng)資源速种,那么應(yīng)該專門編寫一個方法來釋放此種資源姜盈。這樣的類要和其使用者約定:用完資源后必須調(diào)用
close
方法。 - 執(zhí)行異步任務(wù)的方法不應(yīng)在
dealloc
里調(diào)用配阵;只能在正常狀態(tài)下執(zhí)行的那些方法也不應(yīng)在dealloc
里調(diào)用馏颂,因為此時對象已處于正在回收的狀態(tài)了。
第32條:編寫“異常安全代碼”時留意內(nèi)存管理問題
- 在 Objective-C 中棋傍,異常只應(yīng)在發(fā)生嚴重錯誤后拋出(參見第21條)救拉。
- 在
try
塊中,如果先保留了某個對象舍沙,釋放它之前又拋出了異常近上,就會導(dǎo)致內(nèi)存泄漏。而且 ARC 不會自動處理這個問題拂铡。 - 可以在編譯器中開啟
-fobjc-arc-exceptions
(默認關(guān)閉)壹无, 讓 ARC 生成安全處理異常所用的附加代碼。 - Objective-C++模式下感帅,編譯器會自動打開
-fobjc-arc-exceptions
標志斗锭。
要點
- 捕獲異常時,一定要注意將 try 塊內(nèi)所創(chuàng)立的對象清理干凈失球。
- 在默認情況下岖是,ARC 不生成安全處理異常所需的清理代碼。開啟編譯器標志后实苞,可生成這種代碼豺撑,不過會導(dǎo)致應(yīng)用程序變大,而且會降低運行效率黔牵。
第33條:以弱引用避免保留環(huán)
- 幾個對象相互引用會導(dǎo)致引用循環(huán)造成內(nèi)存泄露聪轿。
- 避免引用循環(huán)的最佳方式就是弱引用(weak)。
- unsafe_unretained(不保留也不釋放)猾浦、assign(通常只用于整體類型陆错,如 int灯抛、float、結(jié)構(gòu)體等)音瓷、weak(屬性被回收后會自動設(shè)置為nil) 之間的區(qū)別对嚼?
要點
- 將某些引用設(shè)為
weak
,可避免出現(xiàn)引用循環(huán)绳慎。 -
weak
引用可以自動清空纵竖,也可以不自動清空。自動清空是隨著ARC而引入的新特性偷线,由 runtime 來實現(xiàn)磨确,在具備自動清空功能的弱引用上,可以隨意讀取其數(shù)據(jù)声邦,因為這種引用不會指向已經(jīng)回收過的對象乏奥。
第34條:以“自動釋放池”降低內(nèi)存峰值
自動釋放池(autorelease pool):存放需要在稍后某個時刻釋放的對象。
釋放對象的兩種方式:
- 調(diào)用
release
方法亥曹,使其引用計數(shù)立即遞減邓了; - 調(diào)用
autorelease
方法,將對象加入自動釋放池中媳瞪。清空自動釋放池時骗炉,系統(tǒng)會向其中的對象發(fā)送release
消息;
創(chuàng)建自動釋放池語法:
@autoreleasepool {
// ...
}
程序員無需自己創(chuàng)建自動釋放池蛇受,系統(tǒng)自動創(chuàng)建的線程中默認有自動釋放池句葵。除了 main
函數(shù)中:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
自動釋放池的嵌套:
@autoreleasepool {
NSString *string = [NSString stringWithFormat:@"hello"];
@autoreleasepool {
NSNumber *number = [NSNumber numberWithInteger:1];
}
}
??嵌套自動釋放池可以控制應(yīng)用程序的內(nèi)存峰值,使其不至過高兢仰。
內(nèi)存峰值(high-memory waterline):應(yīng)用程序在某個特定時段內(nèi)的最大內(nèi)存用量乍丈。
NSArray *databaseRecords = /** ... */ ;
NSMutableArray *people = [NSMutableArray new];
// 將循環(huán)內(nèi)的代碼包裹在自動釋放池中
// 系統(tǒng)就會在塊末尾釋放對象,而不是在主線程集中釋放把将。
for (NSDictionary *record in databaseRecords) {
@autoreleasepool {
EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
[people addObject:person];
}
}
??是否應(yīng)該使用自動釋放池優(yōu)化效率還應(yīng)視情況而定轻专,因為創(chuàng)建自動釋放池本身也有一定的開銷。
@autoreleasepool 語法的另一個好處:每個自動釋放池均有其范圍察蹲,可以避免無意間誤用在自動釋放池中已經(jīng)被系統(tǒng)回收的對象请垛。
要點
- 自動釋放池排布在棧中,對象收到
autorelease
消息后洽议,系統(tǒng)將其放入最頂端的池里宗收。 - 合理運用自動釋放池,可降低應(yīng)用程序的內(nèi)存峰值亚兄。
- @autoreleasepool 這種新式寫法能創(chuàng)建出更為輕便的自動釋放池混稽。
第35條:用“僵尸對象”調(diào)試內(nèi)存管理問題
向業(yè)已回收的對象發(fā)送消息是不安全的,可行與否完全取決于對象所占內(nèi)存有沒有為其他內(nèi)容所覆寫。
調(diào)試內(nèi)存管理的最佳方式:僵尸對象(Zombie Object)
??原理:啟用僵尸對象調(diào)試功能之后荚坞,運行期系統(tǒng)會把所有已經(jīng)回收的實例轉(zhuǎn)化成特殊的"僵尸對象",而不會真正回收它們菲盾。這種對象所在的核心內(nèi)存無法重用颓影,因此不可能遭到覆寫。僵尸對象收到消息后懒鉴,會拋出異常诡挂,其中準確說明了發(fā)送過來的消息,并描述了回收之前的那個對象临谱。
將 NszombieEnabled 環(huán)境變量設(shè)為 YES, 即可開啟此功能璃俗。
??位置:Xcode菜單欄 → Product → Scheme → Edit Scheme → Run → Diagnostics診斷選項 → Memory Management → 勾選 Zombie Objects。
?? APP 打包發(fā)布之前一定要取消此勾選悉默!
要點
- 系統(tǒng)在回收對象時城豁,可以不將其真的回收,而是把它轉(zhuǎn)化為僵尸對象抄课。通過環(huán)境變量 NSZombieEnabled 可開啟此功能唱星。
- 系統(tǒng)會修改對象的 isa 指針,令其指向特殊的僵尸類跟磨,從而使該對象變?yōu)榻┦瑢ο蠹淞摹=┦惸軌蝽憫?yīng)所有的selector,響應(yīng)方式為:打印一條包含消息內(nèi)容及其接受者的消息抵拘,然后終止應(yīng)用程序哎榴。
第36條:不要使用 retainCount
- (NSUInteger)retainCount OBJC_ARC_UNAVAILABLE; // 查詢對象當前的保留計數(shù)
?? retainCount
所返回的保留計數(shù)只是某個給定時間點上的值,該方法并未考慮到自動釋放池的情況僵蛛。
// 錯誤示例:
while ([object retainConut]) {
[object release]
}
// 錯誤一:沒有考慮到后續(xù)的自動釋放操作尚蝌;
// 錯誤二:retainConut 可能永遠不返回0;
要點
- 對象的保留計數(shù)看似有用墩瞳,實則不然驼壶,因為任何給定時間點上的"絕對保留計數(shù)"(absoulte retain count)都無法反映對象生命期的全貌。
- 引入 ARC 后喉酌,
retainCount
方法就正式廢止了热凹,在 ARC 下調(diào)用該方法會導(dǎo)致編譯器報錯。