理解 iOS 和 macOS 的內存管理

在 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 之類的,擁有垃圾回收機制的語言,也就慢慢的變得愈來愈受歡迎(大霧🤪🤪🤪)。swift

內存管理基本原則:bash

在須要的時候分配內存,在不須要的時候釋放內存

這裏來一段簡單的 C 代碼~微信

#define BUFFER_SIZE 128

void dosth() {
    char *some_string = malloc(BUFFER_SIZE);
    // 對 some_string 作各類操做
    free(some_string);
}
複製代碼

這麼一句話看起來彷佛不是很複雜,可是光這一個內存管理,管得無數英雄盡折腰啊,由於實際的代碼並不會像上面那麼簡單,好比上面我要把字符串 some_string 返回出來的話要怎麼辦呢?(我不會回答你的👻)markdown

iOS 的內存管理

內存引用計數(Reference Counting,RC)以及 MRC

Objective-C 和 Swift 的內存管理策略都是引用計數,什麼是引用計數呢?下面是 wiki 上摘抄而來的內容:多線程

引用計數是計算機編程語言中的一種內存管理技術,是指將資源(能夠是對象內存磁盤空間等等)的被引用次數保存起來,當被引用次數變爲零時就將其釋放的過程。使用引用計數技術能夠實現自動資源管理的目的。同時引用計數還能夠指使用引用計數技術回收未使用資源的垃圾回收算法。app

當建立一個對象的實例並在堆上申請內存時,對象的引用計數就爲1,在其餘對象中須要持有這個對象時,就須要把該對象的引用計數加1,須要釋放一個對象時,就將該對象的引用計數減1,直至對象的引用計數爲0,對象的內存會被馬上釋放。框架

來源:zh.wikipedia.org/wiki/引用計數

彷佛有點抽象,這裏使用 setter 方法的經典實現做爲例子咱們來看下代碼~

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

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

1

2

3

4

上面的寫法是 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)

可是平時你們會發現歷來沒用過 retainrelease 之類的函數啊?特別是剛入門的同窗,CoreFoundation 也沒有使用過就更納悶了

緣由很簡單,由於這個時代咱們用上了 ARC,ARC 號稱幫助程序員管理內存,而不少人曲解了「幫助」這個詞,在佈道的時候都會說:

ARC 已是自動內存管理了,咱們不須要管理內存

這是一句誤導性的話,ARC 只是幫咱們在代碼中他能夠推斷的部分,自動的添加了 retainrelease 等代碼,可是並不表明他幫咱們管理內存了,實際上 ARC 只是幫咱們省略了部分代碼,在 ARC 沒法推斷的部分,是須要咱們告訴 ARC 如何管理內存的,因此就算是使用 ARC,本質依然是開發者本身管理內存,只是 ARC 幫咱們把簡單狀況搞定了而已

可是,就算是 ARC 僅僅幫咱們把簡單的狀況搞定了,也很是大的程度上解放了你們的創造力、生產力,由於畢竟不少時候內存管理代碼都是會被漏寫的,而且因爲漏寫的時候不必定會發現問題,而是隨着程序運行纔會出現問題,在開發後期解決起來其實挺麻煩的

ARC 下的內存管理

那麼咱們來講說 ARC 中如何進行內存管理,固然核心仍是這句話:對象之間存在持有關係,是否被持有,決定了對象是否被銷燬,固然咱們補充一句話:ARC 中的內存管理,就是理清對象之間的持有關係

strongweak

在上面一節中,其實你們應該發現只寫了 retain,是由於 MRC 的時代只有 retainreleaseautorelease 這幾個手動內存管理的函數。而 strongweak__weak 之類的關鍵字是 Objective-C 2.0 跟着 ARC 一塊兒引入的,能夠認爲他們就是 ARC 時代的內存管理代碼

對於屬性 strongweakassigncopy 告訴 ARC 如何構造屬性對應變量的 setter 方法,對於內存管理的意義來講,就是告訴編譯器對象屬性和對象之間的關係,也就是說平時開發過程當中,一直在使用的 strongweak 其實就是在作內存管理,只是大部分時間你們沒有意識到而已

  • strong:設置屬性時,將會持有(retain)對象
  • weak:設置屬性時,不會持有對象,而且在對象被釋放時,屬性值將會被設置爲 nil
  • assign:設置屬性時,不會持有對象(僅在屬性爲基本類型時使用,由於基本類型不是對象,不存在釋放)
  • copy:設置屬性時,會調用對象的 copy 方法獲取對象的一個副本並持有(對於不可變類型很是有用)

通常狀況下,咱們都會使用 strong 來描述一個對象的屬性,也就是大部分場景下,對象都會持有他的屬性,那麼下面看下不會持有的狀況

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

這裏用經典的 UITableViewDelegateUITableViewDataSource 來進行舉例

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 持有 tableViewstrong 屬性
  2. tableView 沒有持有 conntrollerweak 屬性
  3. 其餘對象持有 controllerstrong 屬性

那麼當第三個關係被打破時,也就是沒有對象持有 controller 了(發生 [controller release],這時候 controller 會釋放他全部的內存,發生下面的事情:

  1. 其餘對象調用 [controller release],沒有對象持有 controllercontroller 開始釋放內存(調用 dealloc
  2. [tableView release],沒有對象持有 tableView 內存被釋放
  3. controller 內存被釋放

由於 weak 屬性不會發生持有關係,因此上面過程完成後,都沒有任何對象持有 tableViewcontroller 因而都被釋放

假設上面對象關係中的 2 變爲 tableView 持有 conntrollerstrong 屬性

那麼當第三個關係被打破時,也就是沒有對象持有 controller 了(發生 [controller release],這時候 controller 會釋放他全部的內存,發生下面的事情:

  • 其餘對象調用 [controller release]tableView 依然持有 controllercontroller 不會釋放內存(不會調用 dealloc

這樣,tableViewcontroller 互相持有,可是沒有任何對象在持有他們,可是他們不會被釋放,由於都有一個對象持有着他們,因而內存泄漏,這種狀況是一種簡單的循環引用

因此,這就是爲何咱們寫的代碼若是會使用到 delegate 模式,須要將 delegate 的屬性設置爲 weak,可是從上面例子咱們能夠理解到,並非 delegate 須要 weak 而是由於出現了 delegate 和使用 delegate 的對象互相持有(循環引用),那麼若是咱們的代碼中不會出現循環引用,那麼使用 weak 反而會出錯(delegate 被過早的釋放),不過這種時候每每有其餘對象會持有 delegate

上面其實只描述了最簡單的循環引用場景,在複雜的場景中,可能會有不少個對象依次持有直到循環,面對各類各樣複雜的場景,本文認爲解決內存問題的方法都是,針對每一個對象,每一個類,理清他們之間的持有關係,也就是:

對象之間存在持有關係,是否被持有,決定了對象是否被銷燬,ARC 中的內存管理,就是理清對象之間的持有關係

__weak__strong

strongweak 是在設置屬性的時候使用的,__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. 因爲 selfdo_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 了啊,爲何依然循環引用了啊?接着繼續看持有關係圖:

持有關係

是否是和上一個場景很像?由於就是同樣的,只是一個視野在類的內部,另外一個視野在類的外部,在類的內部那就是 selfdo_block 互相持有,造成循環引用;在類的外部那就是 dummydo_block 互相持有,造成循環應用

一點我的經驗

實際項目確定不會是本文中這麼明顯簡單的場景,可是再多複雜的場景確定是這些簡單的場景不斷的嵌套組合而成,因此保證代碼內存沒有問題的最好的方法是每次遇到須要處理內存場景時,仔細分析對象間的持有關係,也就是保證組成複雜場景的每一個小場景都沒有問題,那麼基本就不會出現問題了,對於出現內存管理出現問題的狀況,通常咱們都能定位到是某一部分代碼內存泄漏了,那麼直接分析那部分代碼的持有關係是否正確

iOS macOS 開發中的內存管理不要在乎引用計數,引用計數是給運行時看的東西,做爲人類咱們須要在乎對象間的持有關係,理清持有關係那麼就代表引用計數不會有問題

結語

到此對於內存管理的思路算是結束了,可是就像本文一開始所說的,這裏並非結束而是開始,接下來建議你們在有了必定經驗後能夠再去深刻了解下面的內容:

  • Core Foundation 框架的內存管理,沒有 ARC 的眷顧
  • Core Foundation 框架和 Objective-C 的內存交互 —— Toll-Free Bridging,ARC 和 CF 框架的橋樑
  • Objective-C 高級編程 —— 《iOS 與 OS X 多線程和內存管理》,我從這本書裏面收益良多
  • Swift 下的內存管理,分清 weakunowned 有什麼區別,邏輯依然是理清持有關係
  • C 語言入門,Objective-C 源自於 C 語言,全部 C 語言的招式在 Objective-C 中都好用,在某些特殊場景會一定會用到

最後歡迎你們訂閱個人微信公衆號 Little Code

Little Code

  • 公衆號主要發一些開發相關的技術文章
  • 談談本身對技術的理解,經驗
  • 也許會談談人生的感悟
  • 本人不是很高產,可是力求保證質量和原創