说起iOS的OOM问题大家第一想到的应该更多的是内存泄漏(Memory Leak),因为无论是从早期的MRC还是2011年Apple推出的ARC内存泄漏问题一直是iOS开发者比较重视的问题,比如我们熟悉的 Instruments Leaks 分析工具,Xcode 8 推出的 Memory Graph 等都是官方提供的内存泄漏分析工具,除此之外还有类似于[FBRetainCycleDetector](https://github.com/facebook/FBRetainCycleDetector)的第三方工具。不过事实上内存泄漏仅仅是造成OOM问题的一个原因而已,实际开发过程中造成OOM的原因有很多,本文试图从实践的角度来分析造成OOM的诸多情况以及解决办法。

概览

说起iOS的OOM问题大家第一想到的应该更多的是内存泄漏(Memory Leak),因为无论是从早期的MRC还是2011年Apple推出的ARC内存泄漏问题一直是iOS开发者比较重视的问题,比如我们熟悉的 Instruments Leaks 分析工具,Xcode 8 推出的 Memory Graph 等都是官方提供的内存泄漏分析工具,除此之外还有类似于FBRetainCycleDetector的第三方工具。不过事实上内存泄漏仅仅是造成OOM问题的一个原因而已,实际开发过程中造成OOM的原因有很多,本文试图从实践的角度来分析造成OOM的诸多情况以及解决办法。

造成OOM的原因

造成OOM的直接原因是iOS的 Jetsam 机制造成的,在Apple的 Low Memory Reports中解释了具体的运行情况:当内存不足时,系统向当前运行中的App发起applicationDidReceiveMemoryWarning(_ application: UIApplication) 调用和 UIApplication.didReceiveMemoryWarningNotification 通知,如果内存仍然不够用则会杀掉一些后台进程,如果仍然吃紧就会杀掉当前App。

关于 Jetsam 实现机制其实苹果已经开源了XNU代码,可以在这里查看,核心代码在 kern_memorystatus 感兴趣可以阅读,其中包含了很多系统调用函数,可以帮助开发者做一些OOM监控等。

一、内存泄漏

内存泄漏造成内存被持久占用无法释放,对OOM的影响可大可小,多数情况下并非泄漏的类直接造成大内存占用而是无法释放的类引用了比较大的资源造成连锁反应最终形成OOM。一般分析内存泄漏的工具推荐使用Leaks,后来Apple提供了比较方便的Memory Graph。

Instruments Leaks

Leaks应该是被所有开发者推荐的工具,几乎搜索内存泄漏就会提到这个工具,但是很多朋友不清楚其实当前Leaks的作用没有那么大,多数时候内存泄漏使用Leaks是分析不出来的。不妨运行下面的一个再简单不过的泄漏情况(在一个导航控制器Push到下面的控制器然后Pop出去进行验证):

class Demo1ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.customView.block = {
            print(self.view.bounds)
        }
        self.view.addSubview(self.customView)
    }
    
    private lazy var customView:CustomView = {
        let temp = CustomView()
        
        return temp
    }()

    deinit {
        print("Demo1ViewController deinit")
    }
}


class CustomView:UIView {
    var block:(()->Void)?
}

上面这段代码有明显的循环引用造成的内存泄漏,但是前面说的两大工具几乎都无能为力,首先Leaks是:

-w727

网络上有大量的文章去介绍Leaks如何使用等以至于让有些同学以为Leaks是一个无所不能的内存泄漏分析工具,事实上Leaks在当前iOS开发环境下检测出来的内存泄漏比较有限。之所以这样需要先了解一个App的内存包括哪几部分:

  1. Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).

  2. Abandoned memory: Memory still referenced by your application that has no useful purpose.

  3. Cached memory: Memory still referenced by your application that might be used again for better performance.

Leaked memory正是Leaks工具所能发现的内存,这部分内存属于没有任何对象引用的内存,在内存活动图中是是不可达内存。

Abandoned memory在应用内存活动图中存在,但是因为应用程序逻辑问题而无法再次访问的内存。和内存泄漏最主要的区别是它的引用(包括强引用和弱引用)是存在的,但是不会再用了。比如上面的循环引用问题,VC被Pop后这部分内存首先还是在内存活动图中的,但是下次再push我们是创建一个新的VC而非使用原来的VC就造成上一次的VC成了废弃的内存。

如果是早期MRC下创建的对象忘记release之类的使用Leaks是比较容易检测的,但是 ARC 下就比较少了,实际验证过程中发现更多的是引用的一些古老的OC库有可能出现,纯Swift几乎没有。

Abandoned memory事实上要比leak更难发现,关于如何使用Instruments帮助开发者进行废弃的内存分析,参见官方Allocations工具的使用:Find abandoned memory

Memory Graph

当然Xcode 8 的Memory Graph也是一大利器,不过如果你这么想上面的问题很有可能会失望(如下图),事实上Memory Graph我理解有几个问题:第一是这个工具要想实际捕获内存泄漏需要多运行几次,往往一次运行过程是无法捕获到内存泄漏的;第二比如上面的子视图引起的内存泄漏是无法使用它捕获内存泄漏信息的,VC pop之后它会认为VC没有释放它的子视图没有释放也是正确的,事实上VC就应该是被释放的,不过调整一下上面的代码比如删除self.view.addSubview(self.customView)后尽管还存在循环引用但是却是可以检测到的(不过实际上怎么可能那么做呢),关于这个玄学问题没有找到相关的说明文档来解释。但是事实上 Memory graph 从来也没有声明自己是在解决内存泄漏问题,而是内存活动图分析工具,如果这么去想这个问题似乎也不算是什么bug。

-w1440

第三方工具

事实上看到上面的情况相信很多同学会想要使用第三方工具来解决问题,比如大家用的比较多的MLeaksFinderPLeakSniffer,两者不同之处是后者除了可以默认查出 UIViewController 和 UIView 内存泄漏外还可以查出所有UIViewController属性的内存泄漏算是对前者的一个补充。当然前者还配合了 Facebook 的FBRetainCycleDetector可以分析出循环引用出现的引用关系帮助开发者快速修复循环引用问题。

不过可惜的是这两款工具,甚至包括 PLeakSniffer 的 Swift 版本都是不支持 Swift 的(准确的说是不支持Swift 4.2,原因是Swift 4.2继承自 NSObject 的类不会默认添加 @objc 标记 class_copyPropertyList无法访问其属性列表,不仅如此Swift5.x中连添加 @objcMembers 也是没用的),但是 Swift 不是到了5.x才ABI稳定的吗?

版权声明:本文为kenshincui原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/kenshincui/p/13153681.html