0%

如何排查iOS的内存泄漏

常见的内存泄漏示例

ViewController的子视图对self的持有

问题代码:

class WSViewChildLeakTestVC: UIViewController {
var customView: WSCustomView?
deinit {
print("WSViewChildLeakTestVC - deinit()")
}
override func viewDidLoad() {
super.viewDidLoad()
customView = WSCustomView(vc: self)
self.view.addSubview(customView!)
}
}
class WSCustomView: UIView {
var myVC: WSViewChildLeakTestVC?
convenience init(vc: WSViewChildLeakTestVC) {
self.init()
self.myVC = vc
}
}

原因:

两个对象双向强引用,导致两者都不会被释放

解决方法:其中一个对象,改为weak即可

// var myVC: WSViewChildLeakTestVC? 添加weak
weak var myVC: WSViewChildLeakTestVC?

Delegate循环引用

问题代码:

class WSDelegateLeakTestVC: UIViewController,WSCustomDeProtocl {
var childView: WSCustomDeView?
deinit {
print("WSDelegateLeakTestVC - deinit()")
}
override func viewDidLoad() {
super.viewDidLoad()
self.childView = WSCustomDeView()
self.childView?.delegate = self
}
}
protocol WSCustomDeProtocl {
}
class WSCustomDeView: UIView {
var delegate: WSCustomDeProtocl?
}

造成循环引用的原因:

WSDelegateLeakTestVC => ChildView
ChildView.delegate => WSDelegateLeakTestVC

因此也造成了循环引用,导致不能被销毁

解决方法:

代理weak修饰

声明 delegate 为 weak 可能会避免这种情况,但是这样的话会引起编译错误,因为 structs 和 enums 不能引用 weak 变量。该如何解决呢?当声明 protocol 的时候,我们可以指定只有 class 类型的变量可以代理它,这样的话就可以使用 weak 来修饰了。

所以,代码修改如下:

protocol WSCustomDeProtocl: class {
}
class WSCustomDeView: UIView {
weak var delegate: WSCustomDeProtocl?
}

闭包

问题代码:

let someModalVC = WSClosuresLeakTestVC()
/// fix: weak
someModalVC.actionHandler = {
someModalVC.dismiss(animated: true, completion: nil)
}
self.present(someModalVC, animated: true, completion: nil)

原因:

someModalVC <=> actionHandler 互相强引用

解决方法,使用捕获列表:

someModalVC.actionHandler = { [weak self] in
someModalVC?.dismiss(animated: true, completion: nil)
}
// or
someModalVC.actionHandler = { [unowned self] in
someModalVC.dismiss(animated: true, completion: nil)
}

定时器

问题代码:

class WSTimerViewController: UIViewController {
var timer: Timer?
deinit {
print("WSTimerViewController - deinit()")
}
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(scheduledTask), userInfo: nil, repeats: true)
timer?.fire()
}
}

原因:

WSTimerViewController <=> timer 互相强引用

解决方法:

在适当的时候对timer销毁,解除引用

deinit {
timer?.invalidate()
timer = nil
print("WSTimerViewController - deinit()")
}

NSNotificationCenter,KVO 问题

问题代码:

class WSObservableViewController: UIViewController {
deinit {
print("WSObservableViewController - deinit()")
NotificationCenter.default.removeObserver(self)
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(forName: "SomethingToObserverNotification", object: nil, queue: .main, using: handleNotification)
}
private func handleNotification(_ notification: Notification) {
}
}

问题原因:

NSNotificationCenter <=> handleNotification 互相强引用

解决方法,使用捕获列表:

NotificationCenter.default.addObserver(forName: .SomethingToObserveNotification, object: nil, queue: .main) { [weak self] notification in
self?.handleNotification(notification)
}

三种排查内存泄漏方法

静态内存泄漏分析方法(Analyze)

  1. 通过Xcode打开项目,然后点击Product->Analyze,开始进入静态内存泄漏分析
  2. 等待分析结果。
  3. 根据分析的结果对可能造成内存泄漏的代码进行排查

动态内存泄漏分析方法(Leaks)

  1. 通过Xcode打开项目,然后点击Product->Profile,等待build成功之后,选择Leaks
  2. 这时项目开始启动了,由于Leaks是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。橙色矩形框中所示绿色为正常,如果出现红色,则表示出现内存泄漏。
  3. 选中Leaks Checks,在Details所在栏中选择CallTree,并且在右下角勾选Invert Call TreeHide System Libraries,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。

    Debug Memory Graph

Xcode memory graph debugger可以帮助找到和修复循环引用与内存泄露。当被激活时,会暂停app运行,展现当前堆中的对象,对象的关系,对象间的引用。

使用方法:Debugger Navigator -> View Memory Graph Hierarchy

优点:可以轻松地找到一些简单的泄露,比如循环引用。例如一个对象在闭包中持有自己,通过闭包捕获列表可以轻易修复内存泄露。
缺点:可能找不到已经泄露的点。比如,创建一个UIButton对象并在上面添加一个UIToolBars items数组,我们只能看到这发生了内存泄露却看不到为什么泄露。