內(nèi)存泄露
Memory that was allocated at some point, but was never released and is no longer referenced by your app. Since there are no references to it, there’s now no way to release it and the memory can’t be used again.
內(nèi)存泄露指當(dāng)一個(gè)對(duì)象或變量在使用完成后沒(méi)有釋放掉,這個(gè)對(duì)象一直占用著這部分內(nèi)存, 直到應(yīng)用停止。如果這種對(duì)象過(guò)多,內(nèi)存就會(huì)耗盡,程序會(huì)因沒(méi)有內(nèi)存被殺死,即crash。內(nèi)存泄露問(wèn)題在 C++, C 和 Objective-C的 MRC 中是比較普遍的問(wèn)題.。ARC中內(nèi)存泄露問(wèn)題較少,但是由于開(kāi)發(fā)者的不注意,同樣會(huì)出現(xiàn)內(nèi)存泄露,比如:
- 兩個(gè)對(duì)象相互強(qiáng)引用
- 代理
- block
- 通知
- KVO
- 定時(shí)器
注意:從理論上講, 內(nèi)存泄露是由對(duì)象或變量沒(méi)有釋放引起的, 但實(shí)踐證明并非所有的未釋放的對(duì)象或變量都會(huì)導(dǎo)致內(nèi)存泄露, 這與硬件環(huán)境和操作系統(tǒng)系統(tǒng)環(huán)境有關(guān)。
查找泄漏點(diǎn)
在 Xcode 中, 共提供了兩種工具幫助
- Analyze
靜態(tài)分析工具: 可以通過(guò)Product ->Analyze菜單項(xiàng)啟動(dòng)
Analyze主要分析以下四種問(wèn)題:
1、邏輯錯(cuò)誤:訪問(wèn)空指針或未初始化的變量等;
2、內(nèi)存管理錯(cuò)誤:如內(nèi)存泄漏等;
3、聲明錯(cuò)誤:從未使用過(guò)的變量;
4、API調(diào)用錯(cuò)誤:未包含使用的庫(kù)和框架。
這里使用Analyze靜態(tài)分析查找出來(lái)的泄漏點(diǎn),稱之為"可疑泄漏點(diǎn)"。之所以稱之為"可疑泄漏點(diǎn)",是因?yàn)檫@些點(diǎn)未必一定泄露,確認(rèn)這些點(diǎn)是否泄露, 還要通過(guò)Instruments動(dòng)態(tài)分析工具的 Leaks和Allocations跟蹤模板。 Analyze靜態(tài)分析只是一個(gè)理論上的預(yù)測(cè)過(guò)程.
- Instruments
Instruments可以幫我們了解到應(yīng)用程序使用內(nèi)存的幾個(gè)方面:
- 全局內(nèi)存使用情況(Overall Memory Use)
從全局的角度監(jiān)測(cè)應(yīng)用程序的內(nèi)存使用情況,捕捉非預(yù)期的或大幅度的內(nèi)存增長(zhǎng)
- 內(nèi)存泄露(Leaked memory)
未被你的程序引用,同時(shí)也不能被使用或釋放的內(nèi)存
- 廢棄內(nèi)存(Abandoned memory)
被你的程序引用,但是沒(méi)什么用的內(nèi)存
- 僵尸對(duì)象(Zombies)
僵尸對(duì)象指的是對(duì)應(yīng)的內(nèi)存已經(jīng)被釋放并且不再會(huì)使用到,但是你的程序卻在某處依然有指向它的引用。在 iOS 中有一個(gè)NSZombie機(jī)制,這個(gè)是為了內(nèi)存調(diào)試的目的而設(shè)計(jì)的一種機(jī)制。在這個(gè)機(jī)制下,當(dāng)你NSZombieEnabled為 YES 時(shí),當(dāng)一個(gè)對(duì)應(yīng)的引用計(jì)數(shù)減為 0 時(shí),這個(gè)對(duì)象不會(huì)被釋放,當(dāng)這個(gè)對(duì)象再收到任何消息時(shí),它會(huì)記錄一條warning,而不是直接崩潰,以方便我們進(jìn)行程序調(diào)試。
Leaks
查找內(nèi)存泄露的過(guò)程:
1、在Xcode中對(duì)當(dāng)前的項(xiàng)目執(zhí)行Profile (Command-I),并在打開(kāi)的對(duì)話框中選擇Leaks這個(gè)模板:

也可以通過(guò)Xcode->Open Developer Tool->Instrument啟動(dòng)Instruments

2、進(jìn)入Instruments后,選擇正確的設(shè)備和應(yīng)用程序。打開(kāi)界面如下

在Instruments中,雖然選擇了Leaks模板,但默認(rèn)情況下也會(huì)添加Allocations模板?;旧戏彩莾?nèi)存分析都會(huì)使用Allocations模板, 它可以監(jiān)控內(nèi)存分布情況。
3、點(diǎn)擊紅色按鈕運(yùn)行應(yīng)用程序,我們可以看到如下界面:

4、選擇Leak Checks來(lái)查看內(nèi)存泄露
Leaks

其中,綠色勾表示運(yùn)行正常,沒(méi)有內(nèi)存泄露,如果有泄露,會(huì)自動(dòng)顯示紅色x
注意:顯示
紅色x并不代表一定就有內(nèi)存泄露,而且并不一定每次操作都能看到正確定位內(nèi)存泄露部分。因?yàn)锳RC 時(shí)代更常見(jiàn)的內(nèi)存泄露是循環(huán)引用導(dǎo)致的Abandoned memory,而Leaks工具只負(fù)責(zé)檢測(cè)Leaked memory,應(yīng)用有限。
Cycles & Reboots

Call Tree

Canll Tree部分
- Separate By Thread
線程分離,只有這樣才能在調(diào)用路徑中能夠清晰看到占用CPU最大的線程
- Invert Call Tree
從上到下跟蹤堆棧信息.這個(gè)選項(xiàng)可以快捷的看到方法調(diào)用路徑最深方法占用CPU耗時(shí),比如FuncA{FunB{FunC}},勾選后堆棧以C->B->A把調(diào)用層級(jí)最深的C顯示最外面
- Hide System Libraries
這個(gè)就更有用了,勾選后耗時(shí)調(diào)用路徑只會(huì)顯示app耗時(shí)的代碼,性能分析普遍我們都比較關(guān)系自己代碼的耗時(shí)而不是系統(tǒng)的?;臼潜剡x項(xiàng),注意有些代碼耗時(shí)也會(huì)納入系統(tǒng)層級(jí),可以進(jìn)行勾選前后前后對(duì)執(zhí)行路徑進(jìn)行比對(duì)會(huì)非常有用
- Top Functions
按耗時(shí)降序排列
- Flatten Recursion(一般不選)
選上它會(huì)將調(diào)用棧里遞歸函數(shù)作為一個(gè)入口
簡(jiǎn)單的方式可以快速勾選右邊Call Tree中Separate by Thread和Hide System Libraries兩個(gè)選項(xiàng)
實(shí)際Demo
寫(xiě)一個(gè)簡(jiǎn)單的Demo來(lái)實(shí)際查看一下效果
- 創(chuàng)建工程,在
Main.storyboard中選擇NavigationController作為根視圖控制器,在ViewController上添加一個(gè)UITableView并設(shè)置delegate、dataSource - 添加一個(gè)
DetailViewController,實(shí)現(xiàn)代理方法顯示內(nèi)容,點(diǎn)擊cell進(jìn)入詳情頁(yè)面
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
var titles: [String] = []
var images: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
for i in 0..<30 {
titles.append("cell\(i)")
images.append("imageString")
}
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return titles.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
return cell
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.textLabel?.text = titles[indexPath.row]
cell.imageView?.image = UIImage(named: images[indexPath.row])
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let vc = DetailViewController()
self.navigationController?.pushViewController(vc, animated: true)
}
}
- 添加一個(gè)新的
Swift文件,創(chuàng)建Person和Pet兩個(gè)類
import Foundation
class Person {
let name: String
var pet: Pet?
init(name: String) {
self.name = name
}
}
class Pet {
let name: String
var onwer: Person?
init(name: String) {
self.name = name
}
}
-
DetailViewController中實(shí)現(xiàn)循環(huán)引用,這里包括兩個(gè)地方,一個(gè)是定時(shí)器的循環(huán)引用,一個(gè)是對(duì)象之間的循環(huán)引用
import UIKit
class DetailViewController: UIViewController {
var jack:Person!
var dog: Pet!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
jack = Person(name: "Jack")
dog = Pet(name: "dog")
jack.pet = dog
dog.onwer = jack
}
deinit {
print("deinit")
}
}
運(yùn)行Leaks來(lái)查看
運(yùn)行程序之后,點(diǎn)擊進(jìn)入詳情來(lái)回幾次即可
Leaks模式

看不出什么很有用的信息
Cycles & Reboots模式

這里還是非常明顯的,簡(jiǎn)單清晰易讀,Person和Pet相互引用
Call Tree模式

可以通過(guò)雙擊Symbol Name來(lái)定位代碼,也可以選擇對(duì)應(yīng)的行,右鍵Reveal In Xcode
Debug Memory Graph
直接使用Xcode自帶的Debug Memory Graph來(lái)查看內(nèi)存情況。運(yùn)行程序,點(diǎn)擊cell來(lái)回操作幾次,然后點(diǎn)擊Debug Memory Graph

查看結(jié)果

可以明顯的看到對(duì)象之間的循環(huán)引用
第三方內(nèi)存查找?guī)?/h4>
FBRetainCycleDetector是facebook開(kāi)源的一個(gè)用來(lái)檢測(cè)對(duì)象是否有強(qiáng)引用循環(huán)的靜態(tài)庫(kù)。
MLeaksFinder 提供了內(nèi)存泄露檢測(cè)更好的解決方案。只需要引入MLeaksFinder,就可以自動(dòng)在 App運(yùn)行過(guò)程檢測(cè)到內(nèi)存泄露的對(duì)象并立即提醒,無(wú)需打開(kāi)額外的工具,也無(wú)需為了檢測(cè)內(nèi)存泄露而一個(gè)個(gè)場(chǎng)景去重復(fù)地操作。MLeaksFinder 目前能自動(dòng)檢測(cè)UIViewController和UIView對(duì)象的內(nèi)存泄露,而且也可以擴(kuò)展以檢測(cè)其它類型的對(duì)象。
MLeaksFinder 的使用很簡(jiǎn)單,參照 https://github.com/Zepo/MLeaksFinder,基本上就是把 MLeaksFinder 目錄下的文件添加到你的項(xiàng)目中,就可以在運(yùn)行時(shí)(debug 模式下)幫助你檢測(cè)項(xiàng)目里的內(nèi)存泄露了,無(wú)需修改任何業(yè)務(wù)邏輯代碼,而且只在 debug 下開(kāi)啟,完全不影響你的 release 包。
實(shí)現(xiàn)原理可以看MLeaksFinder:精準(zhǔn) iOS 內(nèi)存泄露檢測(cè)工具
iOS內(nèi)存泄漏自動(dòng)檢測(cè)工具PLeakSniffer
推薦使用第三方庫(kù)來(lái)監(jiān)測(cè)內(nèi)存泄漏,開(kāi)發(fā)的時(shí)候快速定位,節(jié)約時(shí)間
參考
Memory Usage Performance Guidelines
Profile your app’s memory usage
Instruments Tutorial with Swift: Getting Started