iOS开发如何定位和解决内存泄漏?

在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。


通过工具定位引用链、针对场景打破强引用循环、规范代码中的引用关系,可有效解决绝大多数内存泄漏问题。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容