前言
最近看了同事整理的一份與內(nèi)存泄漏相關(guān)思維導(dǎo)圖。突然想從內(nèi)存泄漏的角度探討一下與內(nèi)存相關(guān)的話題。
什么是內(nèi)存泄漏硅急?然而我又問自己一個(gè)問題, malloc 的內(nèi)存到底是什么佳遂?
什么是內(nèi)存
在計(jì)算機(jī)系統(tǒng)中营袜,我們談?wù)摰?strong>內(nèi)存通常是指DRAM 。
虛擬內(nèi)存空間
而當(dāng)我們程序在系統(tǒng)上運(yùn)行起來時(shí)丑罪,操作系統(tǒng)為我們提供了一個(gè)假象荚板,程序看起來是獨(dú)占使用處理器、主存和I/O設(shè)備的吩屹。這種假象是通過進(jìn)程的概念來實(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ù)需要在磁盤和主存之間來回傳輸數(shù)據(jù)。
- 為每個(gè)內(nèi)存提供一致的地址空間厌衙。
- 保護(hù)了每個(gè)進(jìn)程的地址空間不被其他進(jìn)程破壞。
上圖是一個(gè) Linux 進(jìn)程的虛擬內(nèi)存绞绒。也就是說我們平時(shí) malloc
得到的內(nèi)存地址婶希,其實(shí)是虛擬內(nèi)存的地址。
動(dòng)態(tài)內(nèi)存分配
其實(shí)系統(tǒng)為我們提供了 mmap
和 munmap
函數(shù)來創(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++ 。
- 隱式分配器壁晒,分配器檢測已分配何時(shí)不再被程序所使用瓷们,那么就釋放這個(gè)塊。例如 Lisp、Java 等谬晕。
什么是內(nèi)存泄漏
內(nèi)存泄漏是常見的內(nèi)存錯(cuò)誤之一碘裕。我們知道 malloc
其實(shí)是從虛擬內(nèi)存空間的堆中申請空閑的地址的。然而內(nèi)存空間是有限的攒钳。程序在運(yùn)行中 malloc
出來的內(nèi)存空間使用完后帮孔,沒有被 free
掉,這樣我們就稱之為內(nèi)存泄漏不撑。
ARC 機(jī)制
iOS 上文兢,不論是 Objective-C 還是 Swift 都是使用引用計(jì)數(shù)式的內(nèi)存管理方式。
ARC 就是 Automatic Reference Counting焕檬, 其實(shí) ARC 很簡單姆坚,我們只需要弄清楚對象之間的持有關(guān)系。
舉個(gè)簡單的例子:
場景一
self.textField.text = @"Sim";
下圖簡單的描述了揩页,這段代碼對象之間的持有關(guān)系旷偿。 self.textField.text
持有著 @"Sim"
。@"Sim"
對象的 retainCount = 1爆侣。
self.textField.text = @"SimCai";
這時(shí)萍程,對象 @"Sim"
不再被 self.textField.text
持有,所以 @"Sim"
對象的 retainCount = 0兔仰,對象會(huì)被釋放掉茫负。
場景二
self.textField.text = @"Sim";
NSString *firstName = self.textField.text;
這段代碼,@"Sim"
對象被 firstName
和 self.textField.text
同時(shí)持有乎赴。所以 @"Sim"
對象的 retainCount = 2 忍法。
self.textField.text = @"SimCai";
這時(shí) self.textField.text
不再持有 @"Sim"
對象,但是 firstName
依然持有 @"Sim"
榕吼,所以 @"Sim"
的 retainCount = 1 饿序,不會(huì)被釋放。
直到 firstName
也不再持有 @"Sim"
對象羹蚣,@"Sim"
才會(huì)被釋放原探。
循環(huán)引用
ARC 整套機(jī)制看起來很簡單,但會(huì)不會(huì)有什么特例顽素,會(huì)造成內(nèi)存無法被正常釋放呢咽弦?
有,循環(huán)引用胁出。
ViewController
持有 TableView
型型,同時(shí) TableView
也持有 ViewController
。 他們相互有引用關(guān)系全蝶。這就是循環(huán)引用闹蒜。
為了打破這種引用的循環(huán)寺枉。我們可以通過 weak
(弱引用) 來解決這個(gè)問題。
一般情況下嫂用,ViewController
是會(huì)被個(gè) UINavigationController
所持有型凳。如果 TableView
也持有 ViewController
,這時(shí) ViewController
的 retainCount = 2嘱函。
而我們對 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ì)別釋放撇寞。
但在一些場景下使用 weak
需要比較注意的顿天。例如,一個(gè)全局的定時(shí)器蔑担,如果持有了 ViewController
是弱引用牌废。 那當(dāng) ViewController
被釋放后,定時(shí)器再去訪問 ViewController
就將引起 crash 啤握。
常見的內(nèi)存泄漏場景
- 使用時(shí) block (需要格外小心)
- NSTimer 沒有銷毀
- KVO 沒有移除
- NSNotification 沒有移除
當(dāng)了解了 ARC 后鸟缕,再我看來這些本質(zhì)都是循環(huán)引用問題(當(dāng)然還有一些 CF 的API,還是需要手動(dòng)內(nèi)存管理的)排抬。
- block 會(huì)捕獲變量
- NSTimer 需要持有對象懂从,進(jìn)行通知回調(diào)
- KVO 需要持有對象,進(jìn)行通知回調(diào)
- NSNotification 需要持有對象蹲蒲,進(jìn)行通知回調(diào)
所以這些操作都容易造成內(nèi)存泄漏番甩。
要避免內(nèi)存泄漏,更重要的是需要了解 ARC 的機(jī)制届搁。實(shí)際上对室,可能造成內(nèi)存泄漏的場景還有很多。
總結(jié)
-
malloc
得到的內(nèi)存地址咖祭,實(shí)際是虛擬內(nèi)存空間中的堆地址。并不是實(shí)際的物理地址蔫骂。 - 內(nèi)存泄漏是指么翰,內(nèi)存資源沒用了,但內(nèi)存資源沒被
free
辽旋。(用完了浩嫌,就別占著坑) - 在 iOS ARC 時(shí)代檐迟,大部分內(nèi)存泄漏問題,是由循環(huán)引用造成的码耐。