iOS 開發(fā):徹底理解 iOS 內(nèi)存管理(MRC 篇)

本文是 「iOS 開發(fā):徹底理解 iOS 內(nèi)存管理」系列的「MRC 篇」。
用來對 Objective-C 語法中,手動管理內(nèi)存 MRC 相關(guān)知識進(jìn)行講解。

1. 什么是內(nèi)存管理

程序在運行的過程中,往往涉及到創(chuàng)建對象、定義變量、調(diào)用函數(shù)或方法,而這些行為都會增加程序的內(nèi)存占用。

而一個移動設(shè)備的內(nèi)存是有限的,每個軟件所能占用的內(nèi)存也是有限的。

當(dāng)程序所占用的內(nèi)存較多時,系統(tǒng)就會發(fā)出內(nèi)存警告,這時就得回收一些不需要再使用的內(nèi)存空間。比如回收一些不需要再使用的對象、變量等。

如果程序占用內(nèi)存過大,系統(tǒng)可能會強(qiáng)制關(guān)閉程序,造成程序崩潰、閃退現(xiàn)象,影響用戶體驗。

所以,我們需要對 「內(nèi)存」 進(jìn)行合理的分配內(nèi)存、清除內(nèi)存,回收不需要再使用的對象。從而保證程序的穩(wěn)定性。

在 iOS 中,我們通常將內(nèi)存分為五大部分:

  • 代碼區(qū):用于存放程序的代碼,即 CPU 執(zhí)行的機(jī)器指令,并且是只讀的。
  • 全局區(qū) / 靜態(tài)區(qū):它主要存放靜態(tài)數(shù)據(jù)、全局?jǐn)?shù)據(jù)和常量。分為未初始化全局區(qū)(BSS 段)、初始化全局區(qū):(數(shù)據(jù)段)。程序結(jié)束后由系統(tǒng)釋放。
    • 數(shù)據(jù)段:用于存放可執(zhí)行文件中已經(jīng)初始化的全局變量,也就是用來存放靜態(tài)分配的變量和全局變量。
    • BSS 段:用于存放程序中未初始化的全局變量。
  • 常量區(qū):用于存儲已經(jīng)初始化的常量。程序結(jié)束后由系統(tǒng)釋放。
  • 棧區(qū)(Stack):用于存放程序臨時創(chuàng)建的變量、存放函數(shù)的參數(shù)值、局部變量等。由編譯器自動分配釋放。
  • 堆區(qū)(Heap):用于存放進(jìn)程運行中被動態(tài)分配的內(nèi)存段。它大小不固定,可動態(tài)擴(kuò)張和縮減。由程序員分配和釋放。

從上邊內(nèi)存的各個部分說明可以看出:只有堆區(qū)存放的數(shù)據(jù)需要由程序員分配和釋放。

堆區(qū)存放的,主要是繼承了 NSObject 的對象,需要由程序員進(jìn)行分配和釋放。其他非對象類型(int、char、float、double、struct、enum 等)則存放在棧區(qū),由系統(tǒng)進(jìn)行分配和釋放。

  • 示例:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10; // 棧
        int b = 20; // 棧
        // p : 棧
        // Person 對象(計數(shù)器 == 1): 堆
        Person *p = [[Person alloc] init];
    }
    // 經(jīng)過上面代碼后, 棧里面的變量 a、b、p 都會被回收
    // 但是堆里面的 Person 對象還會留在內(nèi)存中,因為它是計數(shù)器依然是 1
    return 0;
}
image

2. 內(nèi)存管理機(jī)制

移動端的內(nèi)存管理機(jī)制,主要有三種:

  • 自動垃圾收集(GC)
  • 手工引用計數(shù)和自動釋放池(MRC)
  • 自動引用計數(shù)(ARC)

其中 iOS 運行環(huán)境不支持自動垃圾收集機(jī)制(GC)。蘋果公司使用的是手工引用計數(shù)(MRC)和自動引用計數(shù)(ARC)機(jī)制。

在自動引用計數(shù)(ARC)出現(xiàn)機(jī)制之前,一直是通過手工引用計數(shù)(MRC)機(jī)制這種手寫大量管理代碼的方式來管理內(nèi)存。后來蘋果公司開發(fā)了自動引用計數(shù)(ARC)技術(shù),把這部分工作交給了編譯器來完成,從而大大簡化了開發(fā)工作。但是 ARC 依然還是需要注意循環(huán)引用的問題。

下面來詳細(xì)講解一下「手工引用計數(shù)(MRC)」和「自動引用計數(shù)(ARC)」。


3. MRC 手動管理內(nèi)存(Manual Reference Counting)

3.1 引用計數(shù)器

引用計數(shù)器:
一個整數(shù),表示為「對象被引用的次數(shù)」。系統(tǒng)需要根據(jù)對象的引用計數(shù)器來判斷對象是否需要被回收。

從字面意義上,可以把引用計數(shù)器理解為「對象被引用的次數(shù)」,也可以理解為: 「有多少人正在用這個對象」。

系統(tǒng)根據(jù)引用計數(shù)器的機(jī)制來判斷對象是否需要被回收。在每次 RunLoop 迭代結(jié)束后,都會檢查對象的引用計數(shù)器,如果引用計數(shù)器等于 0,則說明該對象沒有地方繼續(xù)使用它了,可以將其釋放掉。

關(guān)于「引用計數(shù)器」,有以下幾個特點:

  • 每個 OC 對象都有自己的引用計數(shù)器。
  • 任何一個對象,剛創(chuàng)建的時候,初始的引用計數(shù)為 1。
    • 即使用 allocnew 或者 copy 創(chuàng)建一個對象時,對象的引用計數(shù)器默認(rèn)就是 1。
  • 當(dāng)沒有任何人使用這個對象時,系統(tǒng)才會回收這個對象。也就是說:
    • 當(dāng)對象的引用計數(shù)器為 0 時,對象占用的內(nèi)存就會被系統(tǒng)回收。
    • 如果對象的引用計數(shù)器不為 0 時,那么在整個程序運行過程,它占用的內(nèi)存就不可能被回收(除非整個程序已經(jīng)退出)。

3.2 引用計數(shù)器操作

  • 為保證對象的存在,每當(dāng)創(chuàng)建引用到對象需要給對象發(fā)送一條 retain 消息,可以使引用計數(shù)器值 +1 ( retain 方法返回對象本身)。
  • 當(dāng)不再需要對象時,通過給對象發(fā)送一條 release 消息,可以使引用計數(shù)器值 -1。
  • 給對象發(fā)送 retainCount 消息,可以獲得當(dāng)前的引用計數(shù)器值。
  • 當(dāng)對象的引用計數(shù)為 0 時,系統(tǒng)就知道這個對象不再需要使用了,所以可以釋放它的內(nèi)存,通過給對象發(fā)送 dealloc 消息發(fā)起這個過程。
  • 需要注意的是:release 并不代表銷毀 / 回收對象,僅僅是將計數(shù)器 -1。
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 只要創(chuàng)建一個對象默認(rèn)引用計數(shù)器的值就是 1。
        Person *p = [[Person alloc] init];
        NSLog(@"retainCount = %lu", [p retainCount]); // 打印 1

        // 只要給對象發(fā)送一個 retain 消息, 對象的引用計數(shù)器就會 +1。
        [p retain];

        NSLog(@"retainCount = %lu", [p retainCount]); // 打印 2
        // 通過指針變量 p,給 p 指向的對象發(fā)送一條 release 消息。
        // 只要對象接收到 release 消息, 引用計數(shù)器就會 -1。
        // 只要對象的引用計數(shù)器為 0, 系統(tǒng)就會釋放對象。

        [p release];
        // 需要注意的是: release 并不代表銷毀 / 回收對象, 僅僅是將計數(shù)器 -1。
        NSLog(@"retainCount = %lu", [p retainCount]); // 1

        [p release]; // 0
        NSLog(@"--------");
    }
//    [p setAge:20];    // 此時對象已經(jīng)被釋放
    return 0;
}

3.3 dealloc 方法

  • 當(dāng)一個對象的引用計數(shù)器值為 0 時,這個對象即將被銷毀,其占用的內(nèi)存被系統(tǒng)回收。
  • 對象即將被銷毀時系統(tǒng)會自動給對象發(fā)送一條 dealloc 消息(因此,從 dealloc 方法有沒有被調(diào)用,就可以判斷出對象是否被銷毀)
  • dealloc 方法的重寫(注意是在 MRC 中
    • 一般會重寫 dealloc 方法,在這里釋放相關(guān)資源,dealloc 就是對象的遺言
    • 一旦重寫了 dealloc 方法,就必須調(diào)用 [super dealloc],并且放在最后面調(diào)用。
- (void)dealloc {
    NSLog(@"Person dealloc");
    // 注意:super dealloc 一定要寫到所有代碼的最后面
    [super dealloc]; 
}

dealloc 使用注意:

  • 不能直接調(diào)用 dealloc 方法。
  • 一旦對象被回收了, 它占用的內(nèi)存就不再可用,堅持使用會導(dǎo)致程序崩潰(野指針錯誤)。

3.4 野指針和空指針

  • 只要一個對象被釋放了,我們就稱這個對象為「僵尸對象(不能再使用的對象)」。
  • 當(dāng)一個指針指向一個僵尸對象(不能再使用的對象),我們就稱這個指針為「野指針」。
  • 只要給一個野指針發(fā)送消息就會報錯(EXC_BAD_ACCESS 錯誤)。
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init]; // 執(zhí)行完引用計數(shù)為 1。

        [p release]; // 執(zhí)行完引用計數(shù)為 0,實例對象被釋放。
        [p release]; // 此時,p 就變成了野指針,再給野指針 p 發(fā)送消息就會報錯。
        [p release]; // 報錯
    }
    return 0;
}
  • 為了避免給野指針發(fā)送消息會報錯,一般情況下,當(dāng)一個對象被釋放后我們會將這個對象的指針設(shè)置為空指針。
  • 空指針:
    • 沒有指向存儲空間的指針(里面存的是 nil, 也就是 0)。
    • 給空指針發(fā)消息是沒有任何反應(yīng)的。
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init]; // 執(zhí)行完引用計數(shù)為 1。

        [p release]; // 執(zhí)行完引用計數(shù)為 0,實例對象被釋放。
        p = nil; // 此時,p 變?yōu)榱丝罩羔槨?        [p release]; // 再給空指針 p 發(fā)送消息就不會報錯了。
        [p release];
    }
    return 0;
}

3.5 內(nèi)存管理思想

3.5.1 單個對象內(nèi)存管理思想

思想一:自己創(chuàng)建的對象,自己持有,自己負(fù)責(zé)釋放
  • 通過 allocnewcopymutableCopy 方法創(chuàng)建并持有對象。
  • 當(dāng)自己持有的對象不再被需要時,必須調(diào)用 releaseautorelease 方法釋放對象。
id obj = [[NSObject alloc] init];   // 自己創(chuàng)建的對象,自己持有
[obj release];

同樣,new 方法也能持有對象:

id obj = [NSObject new];    // 自己創(chuàng)建的對象,自己持有
[obj release];

而由各類實現(xiàn)的 copyWithZone: 方法和 mutableCopyWithZone: 方法將生成并持有對象的副本。

另外,除了上面四種方法之外,由上面四種方法名稱開頭的方法名,也將生成并持有對象:

  • allocMyObject
  • newMyObject
  • copyMyObject
  • mutableCopyMyObject
思想二:非自己創(chuàng)建的對象,自己也能持有
  • 除了用上面方法(alloc / new / copy / mutableCopy 方法)所取得的的對象,因為非自己生成并持有,所以自己不是該對象的持有者。
  • 通過調(diào)用 retain 方法,即便是非自己創(chuàng)建的對象,自己也能持有對象。
  • 同樣當(dāng)自己持有的對象不再被需要時,必須調(diào)用 release 方法來釋放對象。
id obj = [NSMutableArray array];    // 取得非自己生成的變量,但自己并不持有。
[obj retain];   // 通過 retain 方法持有對象
[obj release];
總結(jié):
  • 無論是否是自己創(chuàng)建的對象,自己都可以持有,并負(fù)責(zé)釋放。
  • 計數(shù)器有加就有減。
  • 曾經(jīng)讓對象的計數(shù)器 +1,就必須在最后讓對象計數(shù)器 -1。

3.5.2 多個對象內(nèi)存管理思想

多個對象之間往往是通過 setter 方法產(chǎn)生聯(lián)系的,其內(nèi)存管理的方法也是在 setter 方法、dealloc 方法中實現(xiàn)的。所以只有了解了 setter 方法是如何實現(xiàn)的,我們才能了解到多個對象之間的內(nèi)存管理思想。接下來我們將從零開始,一步步實現(xiàn) setter 方法,了解多個對象之間的內(nèi)存管理思想。

我們用一個線上斗地主游戲例子來類比一下。假如有一款斗地主游戲,游戲大廳有不同的游戲房間,可供玩家選擇。我們定義游戲房間為 Room 類對象,定義玩家為 Person 類對象,玩家對象擁有 _room 作為成員變量。

一個玩家對象,如果想要玩游戲,就要持有一個房間對象,并保證在使用房間期間,這個房間對象一直存在,并且在游戲房間沒人的時候,還需要將這個房間對象釋放。

根據(jù)上面的描述,我們可以制定以下規(guī)則:

  • 只要一個玩家想使用房間(進(jìn)入房間),就需要對這個游戲房間的引用計數(shù)器 +1。
  • 只要一個玩家不想再使用房間(離開房間),就需要對這個游戲房間的引用計數(shù)器 -1。
  • 只要還有至少一個玩家在用某個房間,那么這個游戲房間就不會被回收,引用計數(shù)至少為 1。
image

下面來定義兩個類 玩家類:Person 和 房間類:Room。

  • 房間類(Room 類)
#import <Foundation/Foundation.h>

@interface Room : NSObject
@property int no; // 房間號
@end
  • 玩家類(Person 類)
#import <Foundation/Foundation.h>
#import "Room.h"

@interface Person : NSObject
{
    Room *_room;
}

- (void)setRoom:(Room *)room;

- (Room *)room;
@end

現(xiàn)在我們通過幾個玩家使用房間的不同應(yīng)用場景來逐步深入理解內(nèi)存管理。

1. 玩家沒有使用房間的情況
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1. 創(chuàng)建兩個對象
        Person *p = [[Person alloc] init];    // 玩家 p
        Room *r = [[Room alloc] init];        // 房間 r
        r.no = 888;    // 房間號賦值

        [r release];    // 釋放房間
        [p release];   // 釋放玩家
    }
    return 0;
}

上述代碼執(zhí)行完第 4~6 行,即:

// 1.創(chuàng)建兩個對象
Person *p = [[Person alloc] init];    // 玩家 p
Room *r = [[Room alloc] init];        // 房間 r
r.no = 888;    // 房間號賦值

之后在內(nèi)存中的表現(xiàn)如下圖所示:

image

可見,Room 實例對象和 Person 實例對象之間沒有相互聯(lián)系,所以各自釋放不會報錯。執(zhí)行完第 8~9 行代碼,即:

[r release];    // 釋放房間
[p release];   // 釋放玩家

后,將房間對象和玩家對象各自釋放掉,在內(nèi)存中的表現(xiàn)如下圖所示:

image

最后各自實例對象的內(nèi)存就會被系統(tǒng)回收。

2. 一個玩家使用一個游戲房間的情況
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1. 創(chuàng)建兩個對象
        Person *p = [[Person alloc] init];    // 玩家 p
        Room *r = [[Room alloc] init];        // 房間 r
        r.no = 888;    // 房間號賦值

        // 將房間賦值給玩家,表示玩家在使用房間
        // 玩家需要使用這間房,只要玩家在,房間就一定要在
        p.room = r; // [p setRoom:r]

        [r release];    // 釋放房間

        // 在這行代碼之前,玩家都沒有被釋放,但是因為玩家還在,那么房間就不能銷毀
        NSLog(@"-----");

        [p release];    // 釋放玩家
    }
    return 0;
}

上邊代碼執(zhí)行完第 4~6 行的時候和之前在內(nèi)存中的表現(xiàn)一樣,如圖所示:

image

當(dāng)執(zhí)行完第 10 行代碼 p.room = r; 時,因為調(diào)用了 setter 方法,將 Room 實例對象賦值給了 Person 的成員變量,不做其他設(shè)置的話,在內(nèi)存中的表現(xiàn)如下圖(做法不對):

image

在調(diào)用 setter 方法的時候,因為 Room 實例對象多了一個 Person 對象引用,所以應(yīng)將 Room 實例對象的引用計數(shù) +1 才對,即 setter 方法應(yīng)該像下邊一樣,對 room 進(jìn)行一次 retain 操作。

- (void)setRoom:(Room *)room { // 調(diào)用 room = r;
    // 對房間的引用計數(shù)器 +1
    [room retain];
    _room = room;
}

那么執(zhí)行完第 10 行代碼 p.room = r;,在內(nèi)存中的表現(xiàn)為:

image

繼續(xù)執(zhí)行第 12 行代碼[r release];,釋放房間,Room 實例對象引用計數(shù) -1,在內(nèi)存中的表現(xiàn)如下圖所示:

image

然后執(zhí)行第 17 行代碼 [p release];,釋放玩家。這時候因為玩家不在房間里了,房間也沒有用了,所以在釋放玩家的時候,要把房間也釋放掉,也就是在 delloc 里邊對房間再進(jìn)行一次 release 操作。

這樣對房間對象來說,每一次 retain / alloc 操作都對應(yīng)一次 release 操作。

- (void)dealloc {
    // 人釋放了, 那么房間也需要釋放
    [_room release];
    NSLog(@"%s", __func__);

    [super dealloc];
}

那么在內(nèi)存中的表現(xiàn)最終如下圖所示:

image

最后實例對象的內(nèi)存就會被系統(tǒng)回收

3. 一個玩家使用一個游戲房間 r 后,換到另一個游戲房間 r2 的情況
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1. 創(chuàng)建兩個對象
        Person *p = [[Person alloc] init];    // 玩家 p
        Room *r = [[Room alloc] init];        // 房間 r
        r.no = 888;    // 房間號賦值

        // 2. 將房間 r 賦值給玩家 p,表示玩家 p 在使用房間 r
        p.room = r; // [p setRoom:r]
        [r release];    // 釋放房間 r

        // 3. 換房
        Room *r2 = [[Room alloc] init];
        r2.no = 444;
        p.room = r2;
        [r2 release];    // 釋放房間 r2

        [p release];    // 釋放玩家 p
    }
    return 0;
}

執(zhí)行下邊幾行代碼:

// 1. 創(chuàng)建兩個對象
Person *p = [[Person alloc] init];    // 玩家 p
Room *r = [[Room alloc] init];        // 房間 r
r.no = 888;    // 房間號賦值

// 2. 將房間 r 賦值給玩家 p,表示玩家 p 在使用房間 r
p.room = r; // [p setRoom:r]
[r release];    // 釋放房間 r

之后的內(nèi)存表現(xiàn)為:

image

接著執(zhí)行換房操作而不進(jìn)行其他操作的話,即:

// 3. 換房
Room *r2 = [[Room alloc] init];
r2.no = 444;
p.room = r2;

后的內(nèi)存表現(xiàn)為:

image

最后執(zhí)行完代碼:

[r2 release];    // 釋放房間 r2
[p release];    // 釋放玩家 p

后的內(nèi)存表現(xiàn)為:

image

可以看出房間 r 并沒有被釋放,這是因為在進(jìn)行換房的時候,并沒有對房間 r 進(jìn)行釋放。所以應(yīng)在調(diào)用 setter 方法的時候,對之前的變量進(jìn)行一次 release 操作。具體 setter 方法代碼如下:

- (void)setRoom:(Room *)room { // room = r
        // 將以前的房間釋放掉 -1
        [_room release];

        // 對房間的引用計數(shù)器 +1
        [room retain];

        _room = room;
    }
}

這樣在執(zhí)行完 p.room = r2; 之后就會將 房間 r 釋放掉,最終內(nèi)存表現(xiàn)為:

image
4. 一個玩家使用一個游戲房間,不再使用游戲房間,將游戲房間釋放掉之后,再次使用該游戲房間的情況
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1. 創(chuàng)建兩個對象
        Person *p = [[Person alloc] init];
        Room *r = [[Room alloc] init];
        r.no = 888;

        // 2. 將房間 r 賦值給玩家 p
        p.room = r; // [p setRoom:r]
        [r release];    // 釋放房間 r

        // 3. 再次使用房間 r
        p.room = r;
        [r release];    // 釋放房間 r
        [p release];    // 釋放玩家 p
    }
    return 0;
}

執(zhí)行下面代碼:

// 1.創(chuàng)建兩個對象
Person *p = [[Person alloc] init];
Room *r = [[Room alloc] init];
r.no = 888;

// 2.將房間賦值給人
p.room = r; // [p setRoom:r]
[r release];    // 釋放房間 r

之后的內(nèi)存表現(xiàn)為:

image

然后再執(zhí)行 p.room = r;,因為 setter 方法會將之前的 Room 實例對象先釋放掉,此時內(nèi)存表現(xiàn)為:

image

此時 _roomr 已經(jīng)變成了一個野指針。之后再對野指針 r 發(fā)出 retain 消息,程序就會崩潰。所以我們在進(jìn)行 setter 方法的時候,要先判斷一下是否是重復(fù)賦值,如果是同一個實例對象,就不需要重復(fù)進(jìn)行 releaseretain。換句話說,如果我們使用的還是之前的房間,那換房的時候就不需要對這個房間再進(jìn)行 releaseretain。則 setter 方法具體代碼如下:

- (void)setRoom:(Room *)room { // room = r
    // 只有房間不同才需用 release 和 retain
    if (_room != room) {    // 0ffe1 != 0ffe1
        // 將以前的房間釋放掉 -1
        [_room release];

        // 對房間的引用計數(shù)器+1
        [room retain];

        _room = room;
    }
}

因為 retain 不僅僅會對引用計數(shù)器 +1, 而且還會返回當(dāng)前對象,所以上述代碼可最終簡化成:

- (void)setRoom:(Room *)room { // room = r
    // 只有房間不同才需用 release 和 retain
    if (_room != room) {    // 0ffe1 != 0ffe1
        // 將以前的房間釋放掉 -1
        [_room release];

        _room = [room retain];
    }
}

以上就是 setter 方法的終極形式。通過上面多個例子,我們也理解了多個對象之間的內(nèi)存管理思想。

3.6 @property 參數(shù)

  • 在成員變量前加上 @property,系統(tǒng)就會自動幫我們生成基本的 setter / getter 方法,但是不會生成內(nèi)存管理相關(guān)的代碼。
@property (nonatomic) int val;
  • 同樣如果在 property 后邊加上 assign,系統(tǒng)也不會幫我們生成 setter 方法內(nèi)存管理的代碼,僅僅只會生成普通的 getter / setter 方法,默認(rèn)什么都不寫就是 assign
@property(nonatomic, assign) int val;
  • 如果在 property 后邊加上 retain,系統(tǒng)就會自動幫我們生成 getter / setter 方法內(nèi)存管理的代碼,但是仍需要我們自己重寫 dealloc 方法。
@property(nonatomic, retain) Room *room;

3.7 自動釋放池

當(dāng)我們不再使用一個對象的時候應(yīng)該將其空間釋放,但是有時候我們不知道何時應(yīng)該將其釋放。為了解決這個問題,Objective-C 提供了 autorelease 方法。

  • autorelease 是一種支持引用計數(shù)的內(nèi)存管理方式,只要給對象發(fā)送一條 autorelease 消息,會將對象放到一個自動釋放池中,當(dāng)自動釋放池被銷毀時,會對池子里面的「所有對象」做一次 release 操作。

注意:這里只是發(fā)送 release 消息,如果當(dāng)時的引用計數(shù)(reference-counted)依然不為 0,則該對象依然不會被釋放。

  • autorelease 方法會返回對象本身,且調(diào)用完 autorelease 方法后,對象的計數(shù)器不變。
Person *p = [Person new];
p = [p autorelease];
NSLog(@"count = %lu", [p retainCount]); // 計數(shù)還為 1

3.7.1 使用 autorelease 有什么好處呢?

  • 不用再關(guān)心對象釋放的時間
  • 不用再關(guān)心什么時候調(diào)用release

3.7.2 autorelease 的原理實質(zhì)上是什么?

autorelease 實際上只是把對 release 的調(diào)用延遲了,對于每一個 autorelease,系統(tǒng)只是把該對象放入了當(dāng)前的 autorelease pool 中,當(dāng)該 pool 被釋放時,該 pool 中的所有對象會被調(diào)用 release 方法。

3.7.3 autorelease 的創(chuàng)建方法

  1. 使用 NSAutoreleasePool 創(chuàng)建
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 創(chuàng)建自動釋放池
[pool release]; // [pool drain]; 銷毀自動釋放池
  1. 使用 @autoreleasepool 創(chuàng)建
@autoreleasepool
{ // 開始代表創(chuàng)建自動釋放池

} // 結(jié)束代表銷毀自動釋放池

3.7.4 autorelease 的使用方法

NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
Person *p = [[[Person alloc] init] autorelease];
[autoreleasePool drain];
@autoreleasepool
{ // 創(chuàng)建一個自動釋放池
        Person *p = [[Person new] autorelease];
        // 將代碼寫到這里就放入了自動釋放池
} // 銷毀自動釋放池(會給池子中所有對象發(fā)送一條 release 消息)

3.7.5 autorelease 的注意事項

  • 并不是放到自動釋放池代碼中,都會自動加入到自動釋放池
@autoreleasepool {
    // 因為沒有調(diào)用 autorelease 方法,所以對象沒有加入到自動釋放池
    Person *p = [[Person alloc] init];
    [p run];
}
  • 在自動釋放池的外部發(fā)送 autorelease 不會被加入到自動釋放池中
    • autorelease 是一個方法,只有在自動釋放池中調(diào)用才有效。
@autoreleasepool {
}
// 沒有與之對應(yīng)的自動釋放池, 只有在自動釋放池中調(diào)用autorelease才會放到釋放池
Person *p = [[[Person alloc] init] autorelease];
[p run];

// 正確寫法
@autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
 }

// 正確寫法
Person *p = [[Person alloc] init];
@autoreleasepool {
    [p autorelease];
}

3.7.6 自動釋放池的嵌套使用

  • 自動釋放池是以棧的形式存在。
  • 由于棧只有一個入口,所以調(diào)用 autorelease 會將對象放到棧頂?shù)淖詣俞尫懦亍?/li>

棧頂就是離調(diào)用 autorelease 方法最近的自動釋放池。

@autoreleasepool { // 棧底自動釋放池
    @autoreleasepool {
        @autoreleasepool { // 棧頂自動釋放池
            Person *p = [[[Person alloc] init] autorelease];
        }
        Person *p = [[[Person alloc] init] autorelease];
    }
}
  • 自動釋放池中不適宜放占用內(nèi)存比較大的對象。
    • 盡量避免對大內(nèi)存使用該方法,對于這種延遲釋放機(jī)制,還是盡量少用。
    • 不要把大量循環(huán)操作放到同一個 @autoreleasepool 之間,這樣會造成內(nèi)存峰值的上升。
// 內(nèi)存暴漲
@autoreleasepool {
    for (int i = 0; i < 99999; ++i) {
        Person *p = [[[Person alloc] init] autorelease];
    }
}
// 內(nèi)存不會暴漲
for (int i = 0; i < 99999; ++i) {
    @autoreleasepool {
        Person *p = [[[Person alloc] init] autorelease];
    }
}

3.7.7 autorelease 錯誤用法

  • 不要連續(xù)調(diào)用 autorelease
@autoreleasepool {
 // 錯誤寫法, 過度釋放
    Person *p = [[[[Person alloc] init] autorelease] autorelease];
 }
  • 調(diào)用 autorelease 后又調(diào)用 release(錯誤)。
@autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
    [p release]; // 錯誤寫法, 過度釋放
}

3.8 MRC 中避免循環(huán)引用

定義兩個類 Person 類和 Dog 類

  • Person 類:
#import <Foundation/Foundation.h>
@class Dog;

@interface Person : NSObject
@property(nonatomic, retain)Dog *dog;
@end
  • Dog 類:
#import <Foundation/Foundation.h>
@class Person;

@interface Dog : NSObject
@property(nonatomic, retain)Person *owner;
@end

執(zhí)行以下代碼:

int main(int argc, const char * argv[]) {
    Person *p = [Person new];
    Dog *d = [Dog new];

    p.dog = d; // retain
    d.owner = p; // retain  assign

    [p release];
    [d release];

    return 0;
}

就會出現(xiàn) A 對象要擁有 B 對象,而 B 對應(yīng)又要擁有 A 對象,此時會形成循環(huán) retain,導(dǎo)致 A 對象和 B 對象永遠(yuǎn)無法釋放。

那么如何解決這個問題呢?

  • 不要讓 A retain B,B retain A。
  • 讓其中一方不要做 retain 操作即可。
  • 當(dāng)兩端互相引用時,應(yīng)該一端用 retain,一端用 assign。

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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