前言
最近看了同事整理的一份與內(nèi)存泄漏相關(guān)思維導圖。突然想從內(nèi)存泄漏的角度探討一下與內(nèi)存相關(guān)的話題。
什么是內(nèi)存泄漏?然而我又問自己一個問題, malloc 的內(nèi)存到底是什么?
什么是內(nèi)存
在計算機系統(tǒng)中,我們談論的內(nèi)存通常是指DRAM 。
虛擬內(nèi)存空間
而當我們程序在系統(tǒng)上運行起來時,操作系統(tǒng)為我們提供了一個假象,程序看起來是獨占使用處理器、主存和I/O設備的。這種假象是通過進程的概念來實現(xiàn)的。
虛擬內(nèi)存,也為進程提供一個假象,每個進程都獨占地使用主存。每個進程看到的內(nèi)存都是一致的,稱為虛擬內(nèi)存空間。
虛擬內(nèi)存的能力:
- 使主存中只保存活動區(qū)域,根據(jù)需要在磁盤和主存之間來回傳輸數(shù)據(jù)。
- 為每個內(nèi)存提供一致的地址空間。
- 保護了每個進程的地址空間不被其他進程破壞。

上圖是一個 Linux 進程的虛擬內(nèi)存。也就是說我們平時 malloc 得到的內(nèi)存地址,其實是虛擬內(nèi)存的地址。
動態(tài)內(nèi)存分配
其實系統(tǒng)為我們提供了 mmap 和 munmap 函數(shù)來創(chuàng)建和刪除虛擬內(nèi)存的區(qū)域。但很多時候直到程序?qū)嶋H運行才知道某些數(shù)據(jù)結(jié)構(gòu)的大小。所以就有了動態(tài)內(nèi)存分配器。
動態(tài)內(nèi)存分配器有兩種基本類型:
- 顯式分配器,需要顯式釋放。例如 C 和 C++ 。
- 隱式分配器,分配器檢測已分配何時不再被程序所使用,那么就釋放這個塊。例如 Lisp、Java 等。
什么是內(nèi)存泄漏
內(nèi)存泄漏是常見的內(nèi)存錯誤之一。我們知道 malloc 其實是從虛擬內(nèi)存空間的堆中申請空閑的地址的。然而內(nèi)存空間是有限的。程序在運行中 malloc 出來的內(nèi)存空間使用完后,沒有被 free 掉,這樣我們就稱之為內(nèi)存泄漏。
ARC 機制
iOS 上,不論是 Objective-C 還是 Swift 都是使用引用計數(shù)式的內(nèi)存管理方式。
ARC 就是 Automatic Reference Counting, 其實 ARC 很簡單,我們只需要弄清楚對象之間的持有關(guān)系。
舉個簡單的例子:
場景一
self.textField.text = @"Sim";
下圖簡單的描述了,這段代碼對象之間的持有關(guān)系。 self.textField.text 持有著 @"Sim"。@"Sim" 對象的 retainCount = 1。

self.textField.text = @"SimCai";
這時,對象 @"Sim" 不再被 self.textField.text 持有,所以 @"Sim" 對象的 retainCount = 0,對象會被釋放掉。

場景二
self.textField.text = @"Sim";
NSString *firstName = self.textField.text;
這段代碼,@"Sim" 對象被 firstName 和 self.textField.text 同時持有。所以 @"Sim" 對象的 retainCount = 2 。

self.textField.text = @"SimCai";
這時 self.textField.text 不再持有 @"Sim" 對象,但是 firstName 依然持有 @"Sim" ,所以 @"Sim" 的 retainCount = 1 ,不會被釋放。

直到 firstName 也不再持有 @"Sim" 對象,@"Sim" 才會被釋放。

循環(huán)引用
ARC 整套機制看起來很簡單,但會不會有什么特例,會造成內(nèi)存無法被正常釋放呢?
有,循環(huán)引用。
ViewController 持有 TableView,同時 TableView 也持有 ViewController 。 他們相互有引用關(guān)系。這就是循環(huán)引用。

為了打破這種引用的循環(huán)。我們可以通過 weak (弱引用) 來解決這個問題。
一般情況下,ViewController 是會被個 UINavigationController 所持有。如果 TableView 也持有 ViewController ,這時 ViewController 的 retainCount = 2。
而我們對 self.vc 使用了 weak 后,self.vc = ViewController ,這樣的操作,不再會導致 retainCount 加1 。 這時 ViewController 依然還是 retainCount = 1。

而當 ViewController 被釋放后 self.vc 建會指向 nil 。當然在這個例子,在 ViewController 釋放后,TableView 自然也會別釋放。

但在一些場景下使用 weak 需要比較注意的。例如,一個全局的定時器,如果持有了 ViewController 是弱引用。 那當 ViewController 被釋放后,定時器再去訪問 ViewController 就將引起 crash 。
常見的內(nèi)存泄漏場景
- 使用時 block (需要格外小心)
- NSTimer 沒有銷毀
- KVO 沒有移除
- NSNotification 沒有移除
當了解了 ARC 后,再我看來這些本質(zhì)都是循環(huán)引用問題(當然還有一些 CF 的API,還是需要手動內(nèi)存管理的)。
- block 會捕獲變量
- NSTimer 需要持有對象,進行通知回調(diào)
- KVO 需要持有對象,進行通知回調(diào)
- NSNotification 需要持有對象,進行通知回調(diào)
所以這些操作都容易造成內(nèi)存泄漏。
要避免內(nèi)存泄漏,更重要的是需要了解 ARC 的機制。實際上,可能造成內(nèi)存泄漏的場景還有很多。
總結(jié)
-
malloc得到的內(nèi)存地址,實際是虛擬內(nèi)存空間中的堆地址。并不是實際的物理地址。 - 內(nèi)存泄漏是指,內(nèi)存資源沒用了,但內(nèi)存資源沒被
free。(用完了,就別占著坑) - 在 iOS ARC 時代,大部分內(nèi)存泄漏問題,是由循環(huán)引用造成的。