内存泄露
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.
内存泄露指当一个对象或变量在使用完成后没有释放掉,这个对象一直占用着这部分内存, 直到应用停止。如果这种对象过多,内存就会耗尽,程序会因没有内存被杀死,即crash。内存泄露问题在 C++, C 和 Objective-C的 MRC 中是比较普遍的问题.。ARC中内存泄露问题较少,但是由于开发者的不注意,同样会出现内存泄露,比如:
- 两个对象相互强引用
- 代理
- block
- 通知
- KVO
- 定时器
注意:从理论上讲, 内存泄露是由对象或变量没有释放引起的, 但实践证明并非所有的未释放的对象或变量都会导致内存泄露, 这与硬件环境和操作系统系统环境有关。
查找泄漏点
在 Xcode 中, 共提供了两种工具帮助
- Analyze
静态分析工具: 可以通过Product ->Analyze
菜单项启动
Analyze
主要分析以下四种问题:
1、逻辑错误:访问空指针或未初始化的变量等;
2、内存管理错误:如内存泄漏等;
3、声明错误:从未使用过的变量;
4、API调用错误:未包含使用的库和框架。
这里使用Analyze
静态分析查找出来的泄漏点,称之为"可疑泄漏点"。之所以称之为"可疑泄漏点",是因为这些点未必一定泄露,确认这些点是否泄露, 还要通过Instruments
动态分析工具的 Leaks
和Allocations
跟踪模板。 Analyze
静态分析只是一个理论上的预测过程.
- Instruments
Instruments
可以帮我们了解到应用程序使用内存的几个方面:
- 全局内存使用情况(Overall Memory Use)
从全局的角度监测应用程序的内存使用情况,捕捉非预期的或大幅度的内存增长
- 内存泄露(Leaked memory)
未被你的程序引用,同时也不能被使用或释放的内存
- 废弃内存(Abandoned memory)
被你的程序引用,但是没什么用的内存
- 僵尸对象(Zombies)
僵尸对象指的是对应的内存已经被释放并且不再会使用到,但是你的程序却在某处依然有指向它的引用。在 iOS 中有一个NSZombie
机制,这个是为了内存调试的目的而设计的一种机制。在这个机制下,当你NSZombieEnabled
为 YES 时,当一个对应的引用计数减为 0 时,这个对象不会被释放,当这个对象再收到任何消息时,它会记录一条warning,而不是直接崩溃,以方便我们进行程序调试。
Leaks
查找内存泄露的过程:
1、在Xcode
中对当前的项目执行Profile (Command-I)
,并在打开的对话框中选择Leaks
这个模板:
也可以通过Xcode->Open Developer Tool->Instrument
启动Instruments
2、进入Instruments
后,选择正确的设备和应用程序。打开界面如下
在Instruments
中,虽然选择了Leaks
模板,但默认情况下也会添加Allocations
模板。基本上凡是内存分析都会使用Allocations
模板, 它可以监控内存分布情况。
3、点击红色按钮运行应用程序,我们可以看到如下界面:
4、选择Leak Checks
来查看内存泄露
Leaks
其中,绿色勾表示运行正常,没有内存泄露,如果有泄露,会自动显示红色x
注意:显示
红色x
并不代表一定就有内存泄露,而且并不一定每次操作都能看到正确定位内存泄露部分。因为ARC 时代更常见的内存泄露是循环引用导致的Abandoned memory
,而Leaks
工具只负责检测Leaked memory
,应用有限。
Cycles & Reboots
Call Tree
Canll Tree部分
- Separate By Thread
线程分离,只有这样才能在调用路径中能够清晰看到占用CPU最大的线程
- Invert Call Tree
从上到下跟踪堆栈信息.这个选项可以快捷的看到方法调用路径最深方法占用CPU耗时,比如FuncA{FunB{FunC}}
,勾选后堆栈以C->B->A
把调用层级最深的C显示最外面
- Hide System Libraries
这个就更有用了,勾选后耗时调用路径只会显示app耗时的代码,性能分析普遍我们都比较关系自己代码的耗时而不是系统的。基本是必选项,注意有些代码耗时也会纳入系统层级,可以进行勾选前后前后对执行路径进行比对会非常有用
- Top Functions
按耗时降序排列
- Flatten Recursion(一般不选)
选上它会将调用栈里递归函数作为一个入口
简单的方式可以快速勾选右边Call Tree中Separate by Thread和Hide System Libraries两个选项
实际Demo
写一个简单的Demo来实际查看一下效果
- 创建工程,在
Main.storyboard
中选择NavigationController
作为根视图控制器,在ViewController
上添加一个UITableView
并设置delegate、dataSource
- 添加一个
DetailViewController
,实现代理方法显示内容,点击cell进入详情页面
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)
}
}
- 添加一个新的
Swift
文件,创建Person
和Pet
两个类
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
中实现循环引用,这里包括两个地方,一个是定时器的循环引用,一个是对象之间的循环引用
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")
}
}
运行Leaks来查看
运行程序之后,点击进入详情来回几次即可
Leaks模式
看不出什么很有用的信息
Cycles & Reboots模式
这里还是非常明显的,简单清晰易读,Person
和Pet
相互引用
Call Tree模式
可以通过双击Symbol Name
来定位代码,也可以选择对应的行,右键Reveal In Xcode
Debug Memory Graph
直接使用Xcode自带的Debug Memory Graph
来查看内存情况。运行程序,点击cell来回操作几次,然后点击Debug Memory Graph
查看结果
可以明显的看到对象之间的循环引用
第三方内存查找库
FBRetainCycleDetector
是facebook
开源的一个用来检测对象是否有强引用循环的静态库。
MLeaksFinder 提供了内存泄露检测更好的解决方案。只需要引入MLeaksFinder
,就可以自动在 App
运行过程检测到内存泄露的对象并立即提醒,无需打开额外的工具,也无需为了检测内存泄露而一个个场景去重复地操作。MLeaksFinder 目前能自动检测UIViewController
和UIView
对象的内存泄露,而且也可以扩展以检测其它类型的对象。
MLeaksFinder 的使用很简单,参照 https://github.com/Zepo/MLeaksFinder,基本上就是把 MLeaksFinder 目录下的文件添加到你的项目中,就可以在运行时(debug 模式下)帮助你检测项目里的内存泄露了,无需修改任何业务逻辑代码,而且只在 debug 下开启,完全不影响你的 release 包。
实现原理可以看MLeaksFinder:精准 iOS 内存泄露检测工具
推荐使用第三方库来监测内存泄漏,开发的时候快速定位,节约时间
参考
Memory Usage Performance Guidelines
Profile your app’s memory usage
Instruments Tutorial with Swift: Getting Started