天天看點

了解 iOS 和 macOS 的記憶體管理什麼是記憶體管理iOS 的記憶體管理結語

在 iOS 和 macOS 應用的開發中,無論是使用 Objective-C 還是使用 swift 都是通過引用計數政策來進行記憶體管理的,但是在日常開發中80%(這裡,我瞎說的,8020 原則嘛)以上的情況,我們不需要考慮記憶體問題,因為 Objective-C 2.0 引入的自動引用計數(ARC)技術為開發者們自動的完成了記憶體管理這項工作。ARC 的出現,在一定程度上拯救了當時剛入門的 iOS 程式員們,如果是沒有接觸過記憶體管理的開發者,在第一次遇到僵屍對象時一定是吓得發抖My Brains~。但是 ARC 隻是在代碼層面上自動添加了記憶體管理的代碼,并不能真正的自動記憶體管理,以及一些高記憶體消耗的特殊場景我們必須要進行手動記憶體管理,是以了解記憶體管理是每一個 iOS 或者 macOS 應用開發者的必備能力。

本文将會介紹 iOS 和 macOS 應用開發過程中,如何進行記憶體管理,以及介紹一些記憶體管理使用的場景,幫助大家解決記憶體方面的問題,本文将會重點介紹記憶體管理的邏輯、思路,而不是類似教你分分鐘手寫

weak

的實作,之類的問題,畢竟大家一般擰螺絲比較多,至于️的制造技藝嘛,還是要靠萬能的 Google 了。

本文其實是記憶體管理的起點,而不是結束,各位 iOS 大佬們肯定會發現很多東西在本文中是找不到的,因為這裡的内容非常基礎,隻是幫助初學 iOS 的同學們能夠快速了解如何管理記憶體而寫的。

什麼是記憶體管理

很多人接觸到記憶體管理可以追溯到大學時候的 C 語言程式設計課程,在大學中為數不多的實踐型語言課程中相信 C 語言以及 C 語言中的指針是很多人的噩夢,并且這個噩夢延續到了 C++,當然這個是後話了。是以 Java 之類的,擁有垃圾回收機制的語言,也就慢慢的變得越來越受歡迎(大霧)。

記憶體管理基本原則:

在需要的時候配置設定記憶體,在不需要的時候釋放記憶體

這裡來一段簡單的 C 代碼~

#define BUFFER_SIZE 128

void dosth() {
    char *some_string = malloc(BUFFER_SIZE);
    // 對 some_string 做各種操作
    free(some_string);
}           

這麼一句話看起來似乎不是很複雜,但是光這一個記憶體管理,管得無數英雄盡折腰啊,因為實際的代碼并不會像上面那麼簡單,比如上面我要把字元串

some_string

傳回出來的話要怎麼辦呢?(我不會回答你的)

iOS 的記憶體管理

記憶體引用計數(Reference Counting,RC)以及 MRC

Objective-C 和 Swift 的記憶體管理政策都是引用計數,什麼是引用計數呢?下面是 wiki 上摘抄而來的内容:

引用計數是計算機 程式設計語言 中的一種記憶體管理技術,是指将資源(可以是 對象 記憶體 磁盤 空間等等)的被 引用 次數儲存起來,當被引用次數變為零時就将其釋放的過程。使用引用計數技術可以實作自動資源管理的目的。同時引用計數還可以指使用引用計數技術回收未使用資源的 垃圾回收

算法。

當建立一個對象的執行個體并在堆上申請記憶體時,對象的引用計數就為1,在其他對象中需要持有這個對象時,就需要把該對象的引用計數加1,需要釋放一個對象時,就将該對象的引用計數減1,直至對象的引用計數為0,對象的記憶體會被立刻釋放。

來源:

https://zh.wikipedia.org/wiki/%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0

似乎有點抽象,這裡使用

setter

方法的經典實作作為例子我們來看下代碼~

- (void)setSomeObject:(NSObject *aSomeObject) {
    if (_someObject != aSomeObject) {
        id oldValue = _someObject;
        _someObject = [aSomeObject retain];  // aSomeObject retain count +1
        [oldValue release];  // oldValue retain count -1
    }
}           

接下來我們圖解下這部分代碼,圖中,矩形為變量(指針),圓圈為實際對象,剪頭表示變量指向的對象

上面的寫法是 MRC 時代的經典方式,這裡就不多說了,因為本文的目的是讓大家了解 ARC 下的記憶體管理。

人工記憶體管理時代 —— Manual Reference Counting(MRC)

人工管理記憶體引用計數的方法叫做 Manual Reference Counting(MRC),在上一節的最後,我們已經看到了記憶體管理的一些些代碼,也看到了記憶體管理時發生了一些什麼,因為 MRC 是 ARC 的基礎,為了更好地了解 ARC,下面是我對 iOS,macOS 下記憶體管理的總結:

對象之間存在持有關系,是否被持有,決定了對象是否被銷毀

也就是說,對于引用計數的記憶體管理,最重要的事情是理清楚對象之間的持有關系,而不關注實際的引用數字,也就是邏輯關系清楚了,那麼實際的引用數也就不會出問題了。

例子

這裡引用《Objective-C 進階程式設計》裡面辦公室的燈的例子,不過我們稍微改改

  1. 自習室有一個燈,燈可以建立燈光,老師要求大家節約用電,隻有在有人需要使用的時候才打開燈
  2. 同學 A 來看書,他打開了燈(建立燈光) —— A 持有燈光
  3. 同學 B,C,D 也來看書,他們也需要燈光 —— B,C,D 分别持有燈光
  4. 這時候 A,B,C 回宿舍了,他們不需要開燈了 —— A,B,C 釋放了燈光
  5. 由于這時候 D 還需要燈光,是以燈一直是打開的 —— D 依然持有燈光
  6. 當 D 離開自習室時 —— D 釋放了燈光
  7. 這時候自習室裡面已經沒有人需要燈光了,于是燈光被釋放了(燈被關了)

上面的例子“燈光”就是我們的被持有的對象,同學們是持有“燈光”的對象,在這個場景,隻要我們理清楚誰持有了“燈光”,那麼我們就能完美的控制“燈光”,不至于沒人的時候“燈光”一直存在導緻浪費電(記憶體洩漏),也不至于有同學需要“燈光”的時候“燈光”被釋放。

這裡看上去很簡單,但是實際項目中将會是這樣的場景不斷的疊加,進而産生非常複雜的持有關系。例子中的同學 A,B,C,D,自習室以及燈也是被其他對象持有的。是以對于最小的一個場景,我們再來一遍:

創造力的解放 —— Automatic Reference Counting(ARC)

但是平時大家會發現從來沒用過

retain

release

之類的函數啊?特别是剛入門的同學,CoreFoundation 也沒有使用過就更納悶了

原因很簡單,因為這個時代我們用上了 ARC,ARC 号稱幫助程式員管理記憶體,而很多人曲解了“幫助”這個詞,在布道的時候都會說:

ARC 已經是自動記憶體管理了,我們不需要管理記憶體

這是一句誤導性的話,ARC 隻是幫我們在代碼中他可以推斷的部分,自動的添加了

retain

release

等代碼,但是并不代表他幫我們管理記憶體了,實際上 ARC 隻是幫我們省略了部分代碼,在 ARC 無法推斷的部分,是需要我們告訴 ARC 如何管理記憶體的,是以就算是使用 ARC,本質依然是開發者自己管理記憶體,隻是 ARC 幫我們把簡單情況搞定了而已

但是,就算是 ARC 僅僅幫我們把簡單的情況搞定了,也非常大的程度上解放了大家的創造力、生産力,因為畢竟很多時候記憶體管理代碼都是會被漏寫的,并且由于漏寫的時候不一定會發現問題,而是随着程式運作才會出現問題,在開發後期解決起來其實挺麻煩的

ARC 下的記憶體管理

那麼我們來說說 ARC 中如何進行記憶體管理,當然核心還是這句話:對象之間存在持有關系,是否被持有,決定了對象是否被銷毀,當然我們補充一句話:ARC 中的記憶體管理,就是理清對象之間的持有關系

strong

weak

在上面一節中,其實大家應該發現隻寫了

retain

,是因為 MRC 的時代隻有

retain

release

autorelease

這幾個手動記憶體管理的函數。而

strong

weak

__weak

之類的關鍵字是 Objective-C 2.0 跟着 ARC 一起引入的,可以認為他們就是 ARC 時代的記憶體管理代碼

對于屬性

strong

weak

assign

copy

告訴 ARC 如何構造屬性對應變量的 setter 方法,對于記憶體管理的意義來說,就是告訴編譯器對象屬性和對象之間的關系,也就是說平時開發過程中,一直在使用的

strong

weak

其實就是在做記憶體管理,隻是大部分時間大家沒有意識到而已

  • strong

    :設定屬性時,将會持有(retain)對象
  • weak

    :設定屬性時,不會持有對象,并且在對象被釋放時,屬性值将會被設定為

    nil

  • assign

    :設定屬性時,不會持有對象(僅在屬性為基本類型時使用,因為基本類型不是對象,不存在釋放)
  • copy

    :設定屬性時,會調用對象的

    copy

    方法擷取對象的一個副本并持有(對于不可變類型非常有用)

一般情況下,我們都會使用

strong

來描述一個對象的屬性,也就是大部分場景下,對象都會持有他的屬性,那麼下面看下不會持有的情況

屬性描述的場景 —— delegate 模式

這裡用經典的

UITableViewDelegate

UITableViewDataSource

來進行舉例

UITableView

的 delegate 和 datasource 應該是學習 iOS 開發過程中最早接觸到的 iOS 中的 delegate 模式

在很多的的例子中,教導我們自己開發的對象,使用的 delegate 的屬性要設定為

weak

的,但是很少有說為什麼(因為循環引用),更少有人會說為什麼會産生循環引用,接下來這裡用

UITableView

的來詳解下

先看

UITableView

中的定義

@interface UITableView : UIScrollView <NSCoding, UIDataSourceTranslating>
// Other Definations ...
@property (nonatomic, weak, nullable) id <UITableViewDataSource> dataSource;
@property (nonatomic, weak, nullable) id <UITableViewDelegate> delegate;
// Other Definations ...
@end           

接下來看下

UITableViewController

中一般的寫法

@interface XXXTableViewController : UITableViewController

@property (nonatomic, strong) UITableView *tableView;

@end

@implementation XXXTableViewController()

- (void)viewDidLoad {
    [super viewDidLoad];
    self.tableView.delegate = self;
    self.tableView.dataSource = self;
}

@end           

下面用一個圖梳理一下持有關系

圖上有三個對象關系

  1. controller

    持有

    tableView

    strong

    屬性
  2. tableView

    沒有持有

    conntroller

    weak

  3. 其他對象持有

    controller

    strong

那麼當第三個關系被打破時,也就是沒有對象持有

controller

了(發生

[controller release]

,這時候

controller

會釋放他所有的記憶體,發生下面的事情:

  1. 其他對象調用

    [controller release]

    ,沒有對象持有

    controller

    controller

    開始釋放記憶體(調用

    dealloc

  2. [tableView release]

    tableView

    記憶體被釋放
  3. controller

因為

weak

屬性不會發生持有關系,是以上面過程完成後,都沒有任何對象持有

tableView

controller

于是都被釋放

假設上面對象關系中的 2 變為

tableView

conntroller

strong

controller

[controller release]

controller

  • [controller release]

    tableView

    依然持有

    controller

    controller

    不會釋放記憶體(不會調用

    dealloc

這樣,

tableView

controller

互相持有,但是沒有任何對象在持有他們,但是他們不會被釋放,因為都有一個對象持有着他們,于是記憶體洩漏,這種情況是一種簡單的循環引用

是以,這就是為什麼我們寫的代碼如果會使用到 delegate 模式,需要将 delegate 的屬性設定為

weak

,但是從上面例子我們可以了解到,并不是 delegate 需要

weak

而是因為出現了 delegate 和使用 delegate 的對象互相持有(循環引用),那麼如果我們的代碼中不會出現循環引用,那麼使用

weak

反而會出錯(delegate 被過早的釋放),不過這種時候往往有其他對象會持有 delegate

上面其實隻描述了最簡單的循環引用場景,在複雜的場景中,可能會有很多個對象依次持有直到循環,面對各種各樣複雜的場景,本文認為解決記憶體問題的方法都是,針對每個對象,每個類,理清他們之間的持有關系,也就是:

對象之間存在持有關系,是否被持有,決定了對象是否被銷毀,ARC 中的記憶體管理,就是理清對象之間的持有關系

__weak

__strong

strong

weak

是在設定屬性的時候使用的,

__weak

__strong

是用于變量的,這兩個關鍵字在開發的過程中不會頻繁的用到,是因為如果沒有指定,那麼變量預設是通過

__strong

修飾的,不過當我們需要使用這兩個關鍵字的時候,那麼也将是我們面對坑最多的情況的時候 —— block 的使用

  • __strong

    :變量預設的修飾符,對應 property 的

    strong

    ,會持有(這裡可以認為是目前代碼塊持有)變量,這裡的持有相當于在變量指派後調用

    retain

    方法,在代碼塊結束時調用

    release

    方法
  • __weak

    :對應 property 的

    weak

    ,同樣在變量被釋放後,變量的值會變成

    nil

變量描述符場景 —— block 的循環引用

下面我們來看個平常經常會遇到的場景,考慮下面的代碼:

// 檔案 Dummy.h
@interface Dummy : NSObject

@property (nonatomic, strong) void (^do_block)();

- (void)do_sth:(NSString *)msg;

@end

// 檔案 Dummy.m
@interface Dummy()
@end

@implementation Dummy

- (void)do_sth:(NSString *)msg {
    NSLog(@"Enter do_sth");
    self.do_block = ^() {
        [self do_sth_inner:msg];
    };
    self.do_block();
    NSLog(@"Exit do_sth");
}

- (void)do_sth_inner:(NSString *)msg {
    NSLog(@"do sth inner: %@", msg);
}

@end

// 檔案 AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    Dummy *dummy = [[Dummy alloc] init];
    [dummy do_sth:@"hello"];
    return YES;
}           

建立一個空白的單頁面 iOS 應用,這裡大家一定知道結果了,在控制台會輸出這樣的内容:

2018-11-15 22:56:34.281346+0800 iOSPlayground[42178:5466855] Enter do_sth
2018-11-15 22:56:34.281445+0800 iOSPlayground[42178:5466855] do sth inner: hello
2018-11-15 22:56:34.281536+0800 iOSPlayground[42178:5466855] Exit do_sth           

當然相信大家已經看出問題來了,上面的代碼會造成循環引用,當然很多時候我們在學習寫 iOS 代碼的時候,都會有人教導過我們 block 裡面的 self 是會存在循環引用的(如上代碼的結果),必須要使用

__weak

,那麼為什麼呢?這裡依然回到上面的記憶體管理原則,我們來梳理一下持有關系,首先這裡有一個基礎知識,那就是 block 是一個對象,并且他會持有所有他捕獲的變量,這裡我們來看下記憶體持有關系:

同樣,我們來分析下這個持有關系

  1. self

    對象持有了

    do_block

  2. 由于

    self

    do_block

    中使用了,是以

    do_block

    的代碼區塊持有了

    self

  3. 其他對象(這裡是

    AppDelegate

    執行個體)通過變量的方式持有對外的

    dummy

那麼在我們的代碼執行到

-application:didFinishLaunchingWithOptions:

最後一行的時候,由于代碼塊的結束,ARC 将會對塊内産生的對象分别調用

release

釋放對象,這時候,上面 3 的持有關系被打破了

但是,由于 1,2 這兩條持有關系存在,是以無論是

self

對象,還是

do_sth

block 他們都至少被一個對象所持有,是以,他們無法被釋放,并且也無法被外界所通路到,形成了循環引用導緻記憶體洩漏,通過 Xcode 提供的記憶體圖(Debug Memeory Graph)我們也可以看到,這一現象:

那麼這裡的解決方法就是,進行下面的修改:

- (void)do_sth:(NSString *)msg {
    NSLog(@"Enter do_sth");
    __weak typeof(self) weakself = self;
    self.do_block = ^() {
        [weakself do_sth_inner:msg];
    };
    self.do_block();
    NSLog(@"Exit do_sth");
}           

這樣打破了上面持有關系 2 中,

do_block

self

的問題,這樣就和上面描述 delegate 的場景一樣了

變量描述符場景 —— block 的循環引用 2

接下來看下另外一個循環引用的場景,

Dummy

類的定義不變,使用方法做一些調整:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    Dummy *dummy = [[Dummy alloc] init];    
    dummy.do_block = ^{
        [dummy do_sth_inner:@"hello2"];
    };
    dummy.do_block();
    return YES;
}           

奇怪,這裡沒有

self

了啊,為什麼依然循環引用了啊?接着繼續看持有關系圖:

是不是和上一個場景很像?因為就是一樣的,隻是一個視野在類的内部,另一個視野在類的外部,在類的内部那就是

self

do_block

互相持有,形成循環引用;在類的外部那就是

dummy

do_block

互相持有,形成循環應用

一點個人經驗

實際項目肯定不會是本文中這麼明顯簡單的場景,但是再多複雜的場景肯定是這些簡單的場景不斷的嵌套組合而成,是以保證代碼記憶體沒有問題的最好的方法是每次遇到需要處理記憶體場景時,仔細分析對象間的持有關系,也就是保證組成複雜場景的每個小場景都沒有問題,那麼基本就不會出現問題了,對于出現記憶體管理出現問題的情況,一般我們都能定位到是某一部分代碼記憶體洩漏了,那麼直接分析那部分代碼的持有關系是否正确

iOS macOS 開發中的記憶體管理不要在意引用計數,引用計數是給運作時看的東西,作為人類我們需要在意對象間的持有關系,理清持有關系那麼就表明引用計數不會有問題

結語

到此對于記憶體管理的思路算是結束了,但是就像本文一開始所說的,這裡并不是結束而是開始,接下來建議大家在有了一定經驗後可以再去深入了解下面的内容:

  • Core Foundation 架構的記憶體管理,沒有 ARC 的眷顧
  • Core Foundation 架構和 Objective-C 的記憶體互動 —— Toll-Free Bridging,ARC 和 CF 架構的橋梁
  • Objective-C 進階程式設計 —— 《iOS 與 OS X 多線程和記憶體管理》,我從這本書裡面收益良多
  • Swift 下的記憶體管理,厘清

    weak

    unowned

    有什麼差別,邏輯依然是理清持有關系
  • C 語言入門,Objective-C 源自于 C 語言,所有 C 語言的招式在 Objective-C 中都好用,在某些特殊場景會必定會用到