什么是內(nèi)存管理戈次?
內(nèi)存管理是程序設(shè)計中常見的資源管理(resource management)的一部分蔑歌。每個計算機系統(tǒng)的可供程序使用的資源都是有限的碌上,包括打開文件均芽、網(wǎng)絡(luò)連接顷蟀、圖片處理等。以圖書館為例骡技,如果每個人都只借不還鸣个,那么圖書館最終因?qū)o書可借而倒閉,其他人也無法再使用圖書館布朦。內(nèi)存管理囤萤,即在程序需要的時候分配內(nèi)存,程序運行結(jié)束時釋放占用內(nèi)存是趴。如果只分配不釋放就會發(fā)生內(nèi)存泄漏(leak memory):程序的內(nèi)存占用不斷增加涛舍,最終耗盡并導(dǎo)致程序崩潰。同時也要注意唆途,不要使用剛釋放的內(nèi)存富雅,避免誤讀陳舊數(shù)據(jù)引發(fā)的各種錯誤。在Cocoa框架中肛搬,通過引用計數(shù)的方式實現(xiàn)內(nèi)存管理没佑。
什么是引用計數(shù)?
Cocoa采用一種叫做引用計數(shù)(reference counting)的技術(shù)管理內(nèi)存温赔。每個對象都有一個與之相關(guān)聯(lián)的整數(shù)蛤奢,被稱作它的引用計數(shù)器。當(dāng)某段代碼需要訪問一個對象時陶贼,該代碼就將該對象的引用計數(shù)器值加1啤贩,表示“我要訪問該對象”。當(dāng)這段代碼結(jié)束對象訪問時拜秧,將對象的引用計數(shù)器值減1痹屹,表示“我不再訪問該對象”。當(dāng)該對象的引用計數(shù)器值為0時枉氮,表示“不再有代碼訪問該對象”志衍。因此暖庄,它將被銷毀,其占用的內(nèi)存被系統(tǒng)收回以便重用足画。
如何使用 Objective-C 進行內(nèi)存管理雄驹?
當(dāng)使用alloc
佃牛、new
方法或者copy
消息創(chuàng)建一個對象時淹辞,對象的引用計數(shù)器值被置為1。需要增加對象的引用計數(shù)器值時俘侠,可以向?qū)ο蟀l(fā)送一條retain
消息象缀。要減少時,向?qū)ο蟀l(fā)送一條release
消息爷速。當(dāng)對象的引用計數(shù)器值歸0時央星,Objective-C會自動向?qū)ο蟀l(fā)送dealloc
消息。(想要獲得當(dāng)前的引用計數(shù)器值惫东,可以向?qū)ο蟀l(fā)送一條retainCount
消息)
等等莉给,這么看的話內(nèi)存管理也不過如此嘛,有啥難的廉沮?那是因為我們還沒考慮對象所有權(quán)(object ownership)颓遏,即某個實體持有一個對象時,該實體就要負責(zé)對其持有的對象進行釋放滞时。
對象所有權(quán)
如果一個對象內(nèi)有指向其他對象的實例變量叁幢,則稱該對象持有這些對象。例如:Car類中包含一個屬性engine坪稽,Car對象持有Engine對象曼玩。同樣如果在一個函數(shù)中創(chuàng)建了一個對象,則稱這個函數(shù)持有該對象窒百。例如:在main()中創(chuàng)建了一個Engine對象黍判,則main()持有該對象。我們已經(jīng)知道了誰持有誰釋放篙梢,接下來看一個例子:
int main(int argc, const char * argv[]) {
Car *car = [Car new];
Engine *engine = [Engine new];
[car setEngine:engine];
return 0;
}
現(xiàn)在哪個實體持有engine對象样悟?main()函數(shù)還是car對象?哪個實體負責(zé)確保當(dāng)engine對象不再被使用時能夠收到release消息庭猩?因為car對象正在使用engine對象窟她,所以不可能是main()函數(shù)。同理mian()函數(shù)后面可能還會使用engine對象蔼水,也不是car對象震糖。
解決辦法讓engine對象的引用計數(shù)器值增加到2。Car類應(yīng)該在setEngine:方法中保留engine對象趴腋,當(dāng)car釋放時在其dealloc方法中釋放engine吊说。
setter方法中的保留與釋放
- (void)setEngine:(Engine *)newEngine {
_engine = [newEngine retain];
}
我們知道Car類setter中需要保留newEngine论咏。但是僅僅保留newEngine是不夠的,比如下面這種情況:
int main(int argc, const char * argv[]) {
Car *car = [Car new];
Engine *engine1 = [Engine new]; // retain count:1
[car setEngine:engine1]; // retain count:2
[engine1 release]; // retain count:1
Engine *engine2 = [Engine new]; // retain count:1
[car setEngine:engine2]; // retain count:2
return 0;
}
我們可以看到[engine1 release]颁井,即mian()已經(jīng)釋放了engine1對象的引用厅贪,Car類也指向新的engine對象,可是engine1對象的引用計數(shù)仍然是1⊙疟觯現(xiàn)在engine1已經(jīng)發(fā)生類內(nèi)存泄漏养涮,engine1會一直空轉(zhuǎn)占用內(nèi)存。
接下來修該setter如下:
- (void)setEngine:(Engine *) newEngine {
[newEngine release];
_engine = [newEngine retain];
}
現(xiàn)在新的setter已經(jīng)修復(fù)了眉抬,engine1對象會內(nèi)存泄漏的問題贯吓。可是這樣還是不夠的蜀变。例如下面這種情況:
int main(int argc, const char * argv[]) {
Engine *engine = [Engine new]; // retain count:1
Car *car1 = [Car new];
Car *car2 = [Car new];
[car1 setEngine:engine]; // retain count:2
[engine release]; // retain count:1
[car2 setEngine:[car1 engine]]; // oops!
return 0;
}
當(dāng)engine和_engine是同一個對象時悄谐,[car1 setEngine:engine]將engine對象的引用計數(shù)器值歸0,并釋放掉engine對象库北。這時再讓car2指向一塊已經(jīng)釋放掉的內(nèi)存就會引發(fā)錯誤爬舰。進一步修改后的setter:
- (void)setEngine:(Engine *) newEngine {
[_engine retain];
[newEngine release];
_engine = newEngine;
}
現(xiàn)在我們已經(jīng)知道setter中應(yīng)該先保留新值,再釋放舊值寒瓦,然后進行賦值情屹。
自動釋放
通過上一篇文章,我們已經(jīng)知道了誰持有誰釋放孵构。如果一個對象由函數(shù)持有就函數(shù)釋放屁商,由某個類持有就讓類來釋放【笔看下面這種情況:
- (NSString *)description {
NSString *description = [[NSString alloc] initWithFormat:@"hello world"];
return description;
}
看上去desctiption方法持有NSString對象description蜡镶,那么description方法應(yīng)該負責(zé)釋放description對象,但是description一旦釋放就無法返回恤筛。這樣就引出了下一個概念:自動釋放池官还。
自動釋放池
Cocoa中有一個自動釋放池(autorelease pool)的概念。我們在程序的入口mian()函數(shù)中都看過關(guān)鍵字@autoreleasepool
毒坛。為了理解自動釋放池的工作望伦,首先要用到NSObject類提供的autorelease
方法:
- (id)autorelease;
該方法的作用是,預(yù)先設(shè)定會在未來某個時間想對象發(fā)送一條release
消息煎殷,其返回值是接接收這條消息的對象屯伞。當(dāng)給一個對象發(fā)送autorelease
消息時,實際上是將該對象添加到了自動釋放池中豪直。當(dāng)自動釋放池唄銷毀時劣摇,會想池中所有對象發(fā)送release
消息。改寫后的代碼如下:
- (NSString *)description {
NSString *description = [[NSString alloc] initWithFormat:@"hello world"];
return [description autorelease];
}
那么我們怎么知道自動釋放池什么時候被銷毀呢弓乙?
自動釋放池銷毀時間
自動釋放池什么時候銷毀末融,并向其包含所有對象發(fā)送release消息钧惧?既然是銷毀,那么創(chuàng)建是在什么時候勾习,如何創(chuàng)建浓瞪?創(chuàng)建自動釋放池有兩種方法:
- 通過
@autoreleasepool
關(guān)鍵字 - 通過
NSAutoreleasePool
對象
1.使用@autoreleasepool{}
時,所有花括號里的代碼都會放入新池子里巧婶。但是要注意乾颁,任何在花括號里定義的變量在括號外就無法使用了。
2.既然NSAutoreleasePool
對象也是NSObject
對象粹舵,同樣遵守引用計數(shù)內(nèi)存管理方式钮孵。如下:
NSAutoreleasePool *pool = [NSAutoreleasePool new];
// 創(chuàng)建對象...
[pool release];
兩種方法推薦使用:@autoreleasepool
關(guān)鍵字骂倘,因為Objective-C語言創(chuàng)建和釋放內(nèi)存的能力遠在我們之上眼滤。下面看一下使用示例:
int main (int argc, const char * argv[])
{
NSAutoreleasePool *pool;
pool = [[NSAutoreleasePool alloc] init];
RetainTracker *tracker;
tracker = [RetainTracker new]; // count: 1
[tracker retain]; // count: 2
[tracker autorelease]; // count: still 2
[tracker release]; // count: 1
NSLog (@"releasing pool");
[pool release];
// gets nuked, sends release to tracker
@autoreleasepool
{
RetainTracker *tracker2;
tracker2 = [RetainTracker new]; // count: 1
[tracker2 retain]; // count: 2
[tracker2 autorelease]; // count: still 2
[tracker2 release]; // count: 1
NSLog (@"auto releasing pool");
}
return (0);
}
注意: [tracker autorelease],向tracker對象發(fā)送autorelease
消息后历涝,tracker對象的引用計數(shù)器值并沒有立即減1诅需,而是保持不變,依舊為2荧库。當(dāng)自動釋放池銷毀時堰塌,將向tracker對象發(fā)送release
消息。運行程序分衫,控制臺輸出結(jié)果為:
init: Retain count of 1.
releasing pool
dealloc called. Bye Bye.
init: Retain count of 1.
auto releasing pool
dealloc called. Bye Bye.
打印結(jié)果驗證了自動釋放池的釋放時間先于其包含的對象场刑。
請記住,自動釋放池被銷毀的時間是確定的:要么是在代碼中你自己手動銷毀蚪战,要么是使用APPKiti時在
事件循環(huán)
結(jié)束時銷毀牵现。
有時即使我們使用了自動釋放池,程序的內(nèi)存卻仍然增長邀桑。如下面這種情況:
int i;
for (i = 0; i < 1000000; i++) {
id obj = [someArray objectAtIndex:i];
NSString *desc = [obj description];
}
該程序執(zhí)行了一個循環(huán)瞎疼,這個循環(huán)創(chuàng)建了100w個desc字符串對象,直到循環(huán)結(jié)束自動釋放池才能釋放壁畸。因為自動釋放池的銷毀時間是確定的贼急,循環(huán)執(zhí)行過程中不會被銷毀。解決這一問題的方法是在循環(huán)中創(chuàng)建自己的自動釋放池捏萍。優(yōu)化代碼如下:
NSAutoreleasePool *pool = [NSAutoreleasePool new];
int i;
for (i = 0; i < 1000000; i++) {
id obj = [someArray objectAtIndex:i];
NSString *desc = [obj description];
if (i % 1000 == 0) {
[pool release];
pool = [NSAutoreleasePool new];
}
}
[pool release];
ARC是什么太抓?
現(xiàn)在我們已經(jīng)掌握了引用計數(shù)管理內(nèi)存的方法,但是日常開發(fā)中幾乎不需要手動管理內(nèi)存——手動引用計數(shù) MRC
(mannul reference counting)令杈,因為蘋果為我們提供了更加高效走敌、安全的管理內(nèi)存方式——自動引用計數(shù) ARC
(automatic reference counting)。ARC像是一位內(nèi)存管家这揣,開啟 ARC
后編譯器會幫助你插入retain
和release
語句悔常。也就是說影斑,ARC
是在編譯時進行工作的。
ARC使用條件:
- 能夠確定哪些對象需要內(nèi)存管理
- 能夠表明如何管理對象
- 有可行的辦法傳遞對象所有權(quán)
橋接轉(zhuǎn)換
日常開發(fā)中99%的內(nèi)存管理工作都交由編譯器了机打,即ARC
矫户。我曾調(diào)侃內(nèi)存管理的使用做多的場景是——面試,這既是玩笑話也是實話残邀。那么還有1%的情況皆辽,即如何對非OC對象進行內(nèi)存管理,這就用到了橋接裝換
(bridge cast)的C語言技術(shù)芥挣。
總結(jié)
Cocoa的內(nèi)存管理規(guī)則:
如果使用new驱闷、alloc、或copy獲得了一個對象空免,則該對象的引用計數(shù)器值為1空另;
如果通過其他方法獲得一個對象,則假設(shè)該對象的引用計數(shù)器值為1蹋砚,而且已經(jīng)被設(shè)置為自動釋放扼菠;
如果保留了某對象,則必須保持retain方法和release方法的使用次數(shù)相等坝咐。
引用:《Objective-C 基礎(chǔ)教程》