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

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

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

場(chǎng)景二
self.textField.text = @"Sim";
NSString *firstName = self.textField.text;
這段代碼,@"Sim" 對(duì)象被 firstName 和 self.textField.text 同時(shí)持有。所以 @"Sim" 對(duì)象的 retainCount = 2 。

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

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

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

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

而當(dāng) ViewController 被釋放后 self.vc 建會(huì)指向 nil 。當(dāng)然在這個(gè)例子,在 ViewController 釋放后,TableView 自然也會(huì)別釋放。

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