iOS基礎(chǔ) | Cocoa內(nèi)存管理

什么是內(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 后編譯器會幫助你插入retainrelease語句悔常。也就是說影斑,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ǔ)教程》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末循榆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子墨坚,更是在濱河造成了極大的恐慌秧饮,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,627評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泽篮,死亡現(xiàn)場離奇詭異盗尸,居然都是意外死亡,警方通過查閱死者的電腦和手機咪辱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評論 3 399
  • 文/潘曉璐 我一進店門振劳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人油狂,你說我怎么就攤上這事历恐。” “怎么了专筷?”我有些...
    開封第一講書人閱讀 169,346評論 0 362
  • 文/不壞的土叔 我叫張陵弱贼,是天一觀的道長。 經(jīng)常有香客問我磷蛹,道長吮旅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,097評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮庇勃,結(jié)果婚禮上檬嘀,老公的妹妹穿的比我還像新娘。我一直安慰自己责嚷,他們只是感情好鸳兽,可當(dāng)我...
    茶點故事閱讀 69,100評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著罕拂,像睡著了一般揍异。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上爆班,一...
    開封第一講書人閱讀 52,696評論 1 312
  • 那天衷掷,我揣著相機與錄音,去河邊找鬼柿菩。 笑死戚嗅,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的碗旅。 我是一名探鬼主播渡处,決...
    沈念sama閱讀 41,165評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼镜悉,長吁一口氣:“原來是場噩夢啊……” “哼祟辟!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起侣肄,我...
    開封第一講書人閱讀 40,108評論 0 277
  • 序言:老撾萬榮一對情侶失蹤旧困,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后稼锅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吼具,經(jīng)...
    沈念sama閱讀 46,646評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,709評論 3 342
  • 正文 我和宋清朗相戀三年矩距,在試婚紗的時候發(fā)現(xiàn)自己被綠了拗盒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,861評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡锥债,死狀恐怖陡蝇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情哮肚,我是刑警寧澤登夫,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站允趟,受9級特大地震影響恼策,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜潮剪,卻給世界環(huán)境...
    茶點故事閱讀 42,196評論 3 336
  • 文/蒙蒙 一涣楷、第九天 我趴在偏房一處隱蔽的房頂上張望分唾。 院中可真熱鬧,春花似錦狮斗、人聲如沸鳍寂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽迄汛。三九已至,卻和暖如春骤视,著一層夾襖步出監(jiān)牢的瞬間鞍爱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評論 1 274
  • 我被黑心中介騙來泰國打工专酗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留睹逃,地道東北人。 一個月前我還...
    沈念sama閱讀 49,287評論 3 379
  • 正文 我出身青樓祷肯,卻偏偏與公主長得像沉填,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子佑笋,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,860評論 2 361

推薦閱讀更多精彩內(nèi)容