在iOS开发中,内存泄漏(Memory Leak)指的是不再需要的对象因被错误地持有强引用而无法被ARC(自动引用计数)回收,导致内存占用持续上升,可能引发App卡顿、崩溃等问题。以下从定位工具、常见场景、解决方法和最佳实践四个维度详细说明。
一、定位内存泄漏的核心工具
内存泄漏的定位依赖工具分析对象的生命周期和引用关系,核心工具包括Xcode自带的调试工具和第三方工具。
1. Xcode Memory Graph(内存图调试)
最直观的定位工具,可实时查看内存中存活的对象及引用链,适合快速定位“不该存在的对象”。
使用步骤:
• 运行App,触发可能发生泄漏的操作(如进入某个页面后返回)。
• 点击Xcode调试栏的「Debug Memory Graph」按钮(或按Cmd+Shift+M)。
• 在左侧面板中筛选目标对象(如ViewController、自定义ViewModel等),查看其是否在“应该释放”时仍存在。
• 选中对象,右侧面板会显示引用链(Retain Cycle或强引用持有者),例如:某个闭包强引用了self,而self又强引用了闭包,形成循环。
2. Instruments(Leaks & Allocations)
更专业的内存分析工具,适合深入追踪泄漏细节(如泄漏发生的堆栈、内存增长趋势)。
Leaks工具:
• 打开Instruments(Xcode → Open Developer Tool → Instruments),选择「Leaks」。
• 选择目标设备和App,点击录制按钮(️),操作App触发泄漏场景。
• 若出现红色泄漏标记,点击「Leaked Objects」,筛选对象后查看「Call Tree」,可定位到创建该对象的代码位置(需勾选「Invert Call Tree」和「Hide System Libraries」过滤系统代码)。
Allocations工具:
• 用于分析对象的分配与释放情况,识别“只分配不释放”的对象。
• 录制时,点击「Mark Generation」(️)标记关键节点(如进入页面时标记一次,返回后再标记一次)。
• 对比两次标记的内存差异,若某类对象数量未减少,可能存在泄漏,通过「Allocation Summary」查看对象的引用链。
3. 第三方工具
• MLeaksFinder(微信团队开源):自动检测UIViewController和UIView的泄漏,当页面返回后若控制器未释放,会弹窗提示,适合开发阶段快速排查。
• FBMemoryProfiler(Facebook开源):可手动触发内存快照,对比对象变化,适合复杂场景分析。
二、常见内存泄漏场景及原因
内存泄漏的核心原因是对象的引用计数始终大于0(即存在未释放的强引用),常见场景如下:
1. 强引用循环(Retain Cycle)
最常见的泄漏原因,指两个或多个对象互相强引用,导致彼此的引用计数无法降为0。
• 场景1:闭包/Block与self的循环引用
闭包默认会强引用捕获的变量,若闭包被self强引用(如self的属性持有闭包),且闭包内部又强引用self,会形成循环。
// 泄漏示例:self持有closure,closure捕获self(强引用)
class MyClass {
var closure: (() -> Void)?
func setup() {
closure = {
self.doSomething() // 闭包强引用self,形成循环
}
}
func doSomething() {}
}
• 场景2:Delegate未用弱引用
若委托方(如ViewController)强引用被委托方(如ScrollView),而被委托方的delegate又被强引用(未用weak),会形成循环。
// 泄漏示例:delegate未用weak
class MyView: UIView {
var delegate: MyDelegate? // 错误:未加weak
}
class MyViewController: UIViewController, MyDelegate {
let myView = MyView()
override func viewDidLoad() {
super.viewDidLoad()
myView.delegate = self // myView强引用delegate(self),self强引用myView → 循环
}
}
• 场景3:NSTimer/定时器的循环引用
Timer会强引用其target(如self),若self又强引用Timer(如作为属性持有),会导致Timer和self互相持有,即使页面销毁也无法释放。
2. 未释放的“全局”强引用
• 单例持有短生命周期对象:单例生命周期与App一致,若单例强引用了某个临时对象(如ViewController),会导致该对象永远无法释放。
• 缓存未清理:如NSCache或自定义缓存中,长期持有不再需要的对象(未设置过期策略或手动移除)。
3. 未注销的观察者/监听器
• 通知(Notification)未移除:注册通知后未在合适时机移除,通知中心会持有观察者(self)的强引用(iOS 9前,通知中心对观察者是强引用;iOS 9后为弱引用,但仍需移除避免野指针)。
• KVO未注销:使用KVO监听对象属性时,若未在对象销毁前调用removeObserver,会导致监听器被持久持有。
4. 系统组件的隐式引用
• WebView相关:UIWebView(已废弃)或WKWebView若未及时置为nil,其内部资源(如JS上下文、代理)可能持有强引用,导致内存泄漏。
• GCD任务未取消:长期运行的异步任务(如DispatchSource)若未在对象销毁前取消,会持续持有self。
三、内存泄漏的解决方法
针对不同场景,核心是打破强引用循环或及时释放不必要的强引用。
1. 解决强引用循环
• 闭包/Block中使用弱引用:
用[weak self](Swift)或__weak typeof(self) weakSelf = self(OC)捕获self,避免闭包强引用self。
// Swift正确写法
func setup() {
closure = { [weak self] in // 弱引用捕获self
self?.doSomething() // 注意:self可能为nil,需用可选链
}
}
若确定self的生命周期长于闭包(如闭包在self销毁前执行完毕),可使用[unowned self](但风险高,self为nil时会崩溃)。
• Delegate用weak修饰:
委托方的delegate必须用weak(Swift)或__weak(OC)修饰,确保不持有委托对象。
// Swift正确写法
class MyView: UIView {
weak var delegate: MyDelegate? // 关键:用weak
}
• NSTimer的正确使用:
用Timer.scheduledTimer(withTimeInterval:repeats:block:)(block版本),避免强引用target;
在对象销毁前(如viewWillDisappear)调用timer.invalidate()并置为nil。
class MyViewController: UIViewController {
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
// block版本timer,内部用weak self
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.update()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
timer?.invalidate() // 必须调用,否则timer会持续持有self
timer = nil
}
}
2. 释放“全局”强引用
• 单例避免持有临时对象:若需传递临时对象,使用弱引用(weak var temp: T?)或在使用后手动置nil。
• 缓存定期清理:对NSCache设置countLimit或totalCostLimit,或在低内存时(UIApplication.didReceiveMemoryWarningNotification)主动清空缓存。
3. 及时注销观察者/监听器
• 通知移除:在deinit(Swift)或dealloc(OC)中移除通知观察者。
deinit {
NotificationCenter.default.removeObserver(self)
}
• KVO注销:在对象销毁前调用removeObserver(_:forKeyPath:)(Swift 4.2+可使用KeyValueObservingController自动管理)。
• WebView清理:不再使用时,将WKWebView的navigationDelegate和uiDelegate置为nil,再将webView本身置为nil。
四、最佳实践
1. 开发阶段主动检测:每次功能开发后,用Memory Graph快速检查页面进出时的对象释放情况。
2. 代码Review关注引用关系:重点检查闭包、delegate、Timer的引用是否正确,避免“隐式强引用”。
3. 利用Swift特性减少风险:优先使用值类型(struct/enum),其无引用计数问题;对必须用class的类型,明确区分强/弱引用。
4. 处理“长生命周期对象”:单例、全局变量等长生命周期对象,避免持有短生命周期对象(如ViewController)的强引用,如需持有,必须用weak。
通过工具定位引用链、针对场景打破强引用循环、规范代码中的引用关系,可有效解决绝大多数内存泄漏问题。