iOS 內存管理MRC

1. 什麼是內存管理

  • 程序在運行的過程當中一般經過如下行爲,來增長程序的的內存佔用
    • 建立一個OC對象
    • 定義一個變量
    • 調用一個函數或者方法
  • 而一個移動設備的內存是有限的,每一個軟件所能佔用的內存也是有限的
  • 當程序所佔用的內存較多時,系統就會發出內存警告,這時就得回收一些不須要再使用的內存空間。好比回收一些不須要使用的對象、變量等
  • 若是程序佔用內存過大,系統可能會強制關閉程序,形成程序崩潰、閃退現象,影響用戶體驗

因此,咱們須要對內存進行合理的分配內存、清除內存,回收那些不須要再使用的對象。從而保證程序的穩定性。java

那麼,那些對象才須要咱們進行內存管理呢?程序員

  • 任何繼承了NSObject的對象須要進行內存管理
  • 而其餘非對象類型(int、char、float、double、struct、enum等) 不須要進行內存管理

這是由於數據結構

  • 繼承了NSObject的對象的存儲在操做系統的裏邊。
  • 操做系統的:通常由程序員分配釋放,若程序員不釋放,程序結束時可能由OS回收,分配方式相似於鏈表
  • 非OC對象通常放在操做系統的裏面
  • 操做系統的:由操做系統自動分配釋放,存放函數的參數值,局部變量的值等。其操做方式相似於數據結構中的棧(先進後出)
  • 示例:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10; // 棧
        int b = 20; // 棧
        // p : 棧
        // Person對象(計數器==1) : 堆
        Person *p = [[Person alloc] init];
    }
    // 通過上面代碼後, 棧裏面的變量a、b、p 都會被回收
    // 可是堆裏面的Person對象還會留在內存中,由於它是計數器依然是1
    return 0;
}
複製代碼
圖片1.png

2. 內存管理模型

提供給Objective-C程序員的基本內存管理模型有如下3種:函數

  • 自動垃圾收集(iOS運行環境不支持)
  • 手工引用計數和自動釋放池(MRC)
  • 自動引用計數(ARC)

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

1. 引用計數器

系統是根據對象的引用計數器來判斷何時須要回收一個對象所佔用的內存ui

  • 引用計數器是一個整數
  • 從字面上, 能夠理解爲」對象被引用的次數」
  • 也能夠理解爲: 它表示有多少人正在用這個對象
  • 每一個OC對象都有本身的引用計數器
  • 任何一個對象,剛建立的時候,初始的引用計數爲1
    • 當使用alloc、new或者copy建立一個對象時,對象的引用計數器默認就是1
  • 當沒有任何人使用這個對象時,系統纔會回收這個對象, 也就是說
    • 當對象的引用計數器爲0時,對象佔用的內存就會被系統回收
    • 若是對象的計數器不爲0,那麼在整個程序運行過程,它佔用的內存就不可能被回收(除非整個程序已經退出 )

2. 引用計數器操做

  • 爲保證對象的存在,每當建立引用到對象須要給對象發送一條retain消息,可使引用計數器值+1 ( retain 方法返回對象自己)
  • 當再也不須要對象時,經過給對象發送一條release消息,可使引用計數器值-1
  • 給對象發送retainCount消息,能夠得到當前的引用計數器值
  • 當對象的引用計數爲0時,系統就知道這個對象再也不須要使用了,因此能夠釋放它的內存,經過給對象發送dealloc消息發起這個過程。
  • 須要注意的是:release並不表明銷燬\回收對象,僅僅是計數器-1
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 只要建立一個對象默認引用計數器的值就是1
        Person *p = [[Person alloc] init];
        NSLog(@"retainCount = %lu", [p retainCount]); // 1

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

        NSLog(@"retainCount = %lu", [p retainCount]); // 2
        // 經過指針變量p,給p指向的對象發送一條release消息
        // 只要對象接收到release消息, 引用計數器就會-1
        // 只要一個對象的引用計數器爲0, 系統就會釋放對象

        [p release];
        // 須要注意的是: release並不表明銷燬\回收對象, 僅僅是計數器-1
        NSLog(@"retainCount = %lu", [p retainCount]); // 1

        [p release]; // 0
        NSLog(@"--------");
    }
// [p setAge:20]; // 此時對象已經被釋放
    return 0;
}
複製代碼

3. dealloc方法

  • 當一個對象的引用計數器值爲0時,這個對象即將被銷燬,其佔用的內存被系統回收
  • 對象即將被銷燬時系統會自動給對象發送一條dealloc消息(所以,從dealloc方法有沒有被調用,就能夠判斷出對象是否被銷燬)
  • dealloc方法的重寫
    • 通常會重寫dealloc方法,在這裏釋放相關資源,dealloc就是對象的遺言
    • 一旦重寫了dealloc方法,就必須調用[super dealloc],而且放在最後面調用
- (void)dealloc
{
    NSLog(@"Person dealloc");
    // 注意:super dealloc必定要寫到全部代碼的最後
    // 必定要寫在dealloc方法的最後面
    [super dealloc]; 
}
複製代碼
  • 使用注意
    • 不能直接調用dealloc方法
    • 一旦對象被回收了, 它佔用的內存就再也不可用,堅持使用會致使程序崩潰(野指針錯誤)

4. 野指針和空指針

  • 只要一個對象被釋放了,咱們就稱這個對象爲 "殭屍對象(不能再使用的對象)"
  • 當一個指針指向一個殭屍對象(不可用內存),咱們就稱這個指針爲野指針
  • 只要給一個野指針發送消息就會報錯(EXC_BAD_ACCESS錯誤)
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init]; // 執行完引用計數爲1 

        [p release]; // 執行完引用計數爲0,實例對象被釋放
        [p release]; // 此時,p就變成了野指針,再給野指針p發送消息就會報錯
        [p release];
    }
    return 0;
}
複製代碼
  • 爲了不給野指針發送消息會報錯,通常狀況下,當一個對象被釋放後咱們會將這個對象的指針設置爲空指針
  • 空指針
    • 沒有指向存儲空間的指針(裏面存的是nil, 也就是0)
    • 給空指針發消息是沒有任何反應的
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init]; // 執行完引用計數爲1

        [p release]; // 執行完引用計數爲0,實例對象被釋放
        p = nil; // 此時,p變爲了空指針
        [p release]; // 再給空指針p發送消息就不會報錯了
        [p release];
    }
    return 0;
}
複製代碼

5. 內存管理規律

單個對象內存管理規律

  • 誰建立誰release :
    • 若是你經過alloc、new、copy或mutableCopy來建立一個對象,那麼你必須調用release或autorelease
  • 誰retain誰release:
    • 只要你調用了retain,就必須調用一次release
  • 總結一下就是
    • 有加就有減
    • 曾經讓對象的計數器+1,就必須在最後讓對象計數器-1

多個對象內存管理規律

由於多個對象之間每每是聯繫的,因此管理起來比較複雜。這裏用一個玩遊戲例子來類比一下。spa

遊戲能夠提供給玩家(A類對象) 遊戲房間(B類對象)來玩遊戲。操作系統

  • 只要一個玩家想使用房間(進入房間),就須要對這個房間的引用計數器+1
  • 只要一個玩家不想再使用房間(離開房間),就須要對這個房間的引用計數器-1
  • 只要還有至少一個玩家在用某個房間,那麼這個房間就不會被回收,引用計數至少爲1
圖片2.png

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

房間類:Room,房間類中有房間號指針

#import <Foundation/Foundation.h>

@interface Room : NSObject
@property int no; // 房間號
@end

複製代碼

玩家類:Personcode

#import <Foundation/Foundation.h>
#import "Room.h"

@interface Person : NSObject
{
    Room *_room;
}

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

- (Room *)room;
@end

複製代碼

如今咱們經過幾個玩家使用房間的不一樣應用場景來逐步深刻理解內存管理。

1. 玩家沒有使用房間,玩家和房間之間沒有聯繫的狀況

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1.建立兩個對象
        Person *p = [[Person alloc] init];    // 玩家 p
        Room *r = [[Room alloc] init];        // 房間 r
        r.no = 888;    // 房間號賦值

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

上述代碼執行完前3行

// 1.建立兩個對象
        Person *p = [[Person alloc] init];    // 玩家 p
        Room *r = [[Room alloc] init];        // 房間 r
        r.no = 888;    // 房間號賦值
複製代碼

以後在內存中的表現以下圖所示:

圖片3.png

可見,Room實例對象和Person實例對象之間沒有相互聯繫,因此各自釋放不會報錯。執行完四、5行代碼

[r release];    // 釋放房間 
        [p release];   // 釋放玩家
複製代碼

後,將房間對象和玩家對象各自釋放掉,在內存中的表現以下圖所示:

圖片4.png

最後各自實例對象的內存就會被系統回收

2. 一個玩家使用一個遊戲房間,玩家和房間之間相關聯的狀況

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1.建立兩個對象
        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;
}

複製代碼

上邊代碼執行完前3行的時候和以前在內存中的表現同樣,如圖

圖片3.png

當執行完第4行代碼p.room = r;時,由於調用了setter方法,將Room實例對象賦值給了Person的成員變量,不作其餘設置的話,在內存中的表現以下圖(作法不對):

圖片5.png

在調用setter方法的時候,由於Room實例對象多了一個Person對象引用,因此應將Room實例對象的引用計數+1纔對,即setter方法應該像下邊同樣,對room進行一次retain操做。

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

複製代碼

那麼執行完第4行代碼p.room = r;,在內存中的表現爲:

圖片6.png

繼續執行第5行代碼[r release];,釋放房間,Room實例對象引用計數-1,在內存中的表現以下圖所示:

圖片5.png

而後執行第6行代碼[p release];,釋放玩家。這時候由於玩家不在房間裏了,房間也沒有用了,因此在釋放玩家的時候,要把房間也釋放掉,也就是在delloc裏邊對房間再進行一次release操做。

這樣對房間對象來講,每一次retain/alloc操做都對應一次release操做。

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

    [super dealloc];
}

複製代碼

那麼在內存中的表現最終以下圖所示:

圖片7.png

最後實例對象的內存就會被系統回收

3. 一個玩家使用一個遊戲房間r後,換到另外一個遊戲房間r2,玩家和房間相關聯的狀況

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1.建立兩個對象
        Person *p = [[Person alloc] init];    // 玩家 p
        Room *r = [[Room alloc] init];        // 房間 r
        r.no = 888;    // 房間號賦值 

        // 2.將房間賦值給玩家,表示玩家在使用房間
        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;
}

複製代碼

執行下邊幾行代碼

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

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

複製代碼

以後的內存表現爲:

圖片8.png

接着執行換房操做而不進行其餘操做的話,

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

內存的表現爲:

圖片9.png

最後執行完

[r2 release];    // 釋放房間 r2
        [p release];    // 釋放玩家 p
複製代碼

內存的表現爲:

圖片10.png

能夠看出房間 r 並無被釋放,這是由於在進行換房的時候,並無對房間 r 進行釋放。因此應在調用setter方法的時候,對以前的變量進行一次release操做。具體setter方法代碼以下:

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

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

        _room = room;
    }
}

複製代碼

這樣在執行完p.room = r2;以後就會將 房間 r 釋放掉,最終內存表現爲:

圖片11.png

4. 一個玩家使用一個遊戲房間,再也不使用遊戲房間,將遊戲房間釋放掉以後,再次使用該遊戲房間的狀況

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1.建立兩個對象
        Person *p = [[Person alloc] init];
        Room *r = [[Room alloc] init];
        r.no = 888;

        // 2.將房間賦值給人
        p.room = r; // [p setRoom:r]
        [r release];    // 釋放房間 r 
        
        // 3.再次使用房間 r
        p.room = r;
        [r release];    // 釋放房間 r 
        [p release];    // 釋放玩家 p
    }
    return 0;
}
複製代碼

執行下面代碼

// 1.建立兩個對象
        Person *p = [[Person alloc] init];
        Room *r = [[Room alloc] init];
        r.no = 888;

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

以後的內存表現爲:

圖片12.png

而後再執行p.room = r;,由於setter方法會將以前的Room實例對象先release掉,此時內存表現爲:

圖片13.png

此時_room、r 已經變成了一個野指針。以後再對野指針 r 發出retain消息,程序就會崩潰。因此咱們在進行setter方法的時候,要先判斷一下是不是重複賦值,若是是同一個實例對象,就不須要重複進行release和retain。換句話說,若是咱們使用的仍是以前的房間,那換房的時候就不須要對這個房間再進行release和retain。則setter方法具體代碼以下:

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

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

        _room = room;
    }
}
複製代碼

由於retain不只僅會對引用計數器+1, 並且還會返回當前對象,因此上述代碼可最終簡化成:

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

        _room = [room retain];
    }
}
複製代碼

以上就是setter方法的最終形式。

轉自:https://www.jianshu.com/p/48665652e4e4
相關文章
相關標籤/搜索