iOS中内存泄漏问题概况和面对

1,584 阅读7分钟

1. 本文内容

在开始之前,想声明我讨厌的一点。在一些程序员博客里和他人讲一些编程的概念的时候,会很喜欢一根筋钻到底地去分析一件事情的来龙去脉。这曾经在我的学习早期带来了很大的困扰。我认为,在学习编程的过程中,这是一件很糟糕的事情,合理的抽象让我们理解编程变得容易。最近看到王垠的博客有感而发:talk is not cheap. 尤其是这个talk的抽象程度是处于一个比较合适的时候。所以,在这里我会声明一下本文目的,有了目的,那么我们在重点关注的事情上也就有了一致的意见。

目的: 内存泄漏是在iOS开发中经常会遇到的问题,本文会大概讲解一下内存和内存泄漏的概念,并且提供一些实用的解决办法。

2. 内存是什么,内存泄漏是什么?

程序员的开发就是为了解决某些问题而存在,而为了解决这些问题,唯一办法就是创建许多变量,然后通过一系列的计算,来完成我们的任务。在程序运行中,变量会存储在计算机的内存中。既然依托于某个载体,那么这个载体必然是有一个上限的,比如我们经常听到国产手机大内存8G,16G之类,这就是内存。然后现代内存无论这个数字多大,总归是有上限的,如果我们无限制地使用它,那么最终就会导致没有内存可以用了,程序也就崩溃了。素以内存的管理是必须存在且有必要的。那么什么是内存泄漏呢?内存泄漏是因为内存管理不当,导致该被释放的内存资源没有被释放掉。他并不会马上导致程序崩溃,但是内存泄漏导致的资源不被回收会不断地提高内存的使用率,当到达了程序的内存上限时,那么程序就崩溃了。

3. 内存泄漏怎么发生的?

想知道内存泄漏是怎么发生的,那么我们就必须要知道内存是怎么被管理的。

iOS中的内存管理有过两个阶段。

  1. MRC(多年前常用的内存管理方法)
  2. ARC(当前的绝大部分OC和Swift对象都是用ARC管理的)

他们都是利用引用计数法来完成内存的管理。

系统会为每个对象维护一个引用计数的值,并且系统会在合适的时机去检测对象的引用计数。如果系统检测到引用计数为0,那么就会释放该片内存上的资源。

如何管理这个引用计数呢?retain + 1, release - 1.

MRC需要手动添加类似代码

- (void)someFunc {
    XYS *someXys = [XYS new];
    [someXys retain];
    [someXys DoSomeThing];
    [someXys release];
}

这样在someFunc结束之后,你的someXys的引用计数就会变为0, 占用的内存资源就会被回收掉。

而在ARC环境中,你只需要这么做

- (void)someFunc {
    XYS *someXys = [XYS new];
    [someXys DoSomeThing];
}

同样,这里的资源也会被回收。

为什么它能被正确回收呢,因为编译器认为你的someXys是在这里创建的,出了这个函数之后,你的someXys就没有必要存在了,他自动地把retain和release的相关代码插入到指定的位置。完成了这个资源分配和回收的过程。

在MRC环境下,你如果忘了加上你的retain,release, 就会导致该释放的资源没有正确释放,于是就内存无法被释放, 于是导致内存泄漏。

那么在ARC环境下,有什么情况会让这个系统失灵呢?

情况太多了。

4. 常见的泄漏原因

  1. 循环引用(最常见原因)

    ARC中有一个持有的概念,对普通成员变量赋值的时候,这个被持有的对象就被持有了,被持有对象引用计数+1, 而如果两个对象互相持有的话,那么双方都会无法被释放。这是ARC的在设计持有这个概念的时候,无法避免的缺陷。如何解决?如果持有关系不增加引用计数呢?那肯定不行,那这样无法区分哪些资源是有用的,哪些是没用的。所以ARC中引入弱持有的概念。我们把增加引用计数的持有关系叫强持有,不增加引用计数的关系叫弱持有。如果将其中一个持有关系声明为弱持有,那么就可以解决这个循环引用的问题。

    还有一种情况是,如果一个闭包是被强持有,而闭包中出现了其他对象,那么闭包中的对象也会被强持有。这种情况的处理方法和上面是基本一样的,也是给个声明,然后指定闭包中某些变量不会被强持有。

    声明弱持有的关键字是weak,在OC和Swift中使用略有不同,其使用有许多的资料,就不多赘述。

    在iOS中有许多常见可能造成循环引用场景:

     1.1 NSTimer的target-action行为会强持有target, runloop强持有timer. 如果没有主动销毁timer,  会导致循环引用
    
     1.2 WKWebView中的新增scriptMessageHandler时delegate是强持有
    
     1.3 以及其他,这类的来源是一些隐形的强持有
    
  2. 单例使用闭包的情况.

    单例在持有闭包的时候,闭包中的所有对象都会同时被延长相应的生命周期,这一点也需要注意。

  3. 非OC或者Swift代码出现内存泄漏(较少的情况)

    可以认为这一部分是基本不用处理的,因为C对象,C++对象是不属于iOS开发的范畴,他们的内存分配属于那片领域的事情了。如果你的App里出现了非ARC对象的问题,那么你应该去学习一下在对应语言环境下内存管理的门路。

5. 如何检测?

先抛出一个结论,内存泄漏在iOS开发中几乎是无法避免的。因为程序给了开发人员操作内存空间的自由,什么时候分配什么时候释放都是由开发人员决定的。现在的计算机没有“智能”到能自动检测在特定环境下,哪些是应该被分配,哪些是应该被释放的。很多时候都取决于开发人员的细心程度。但现在已经是9102年,我们总不能脑袋里天天蹦着根弦天天追着内存问题不放吧?有没有工具能自动检测的?

  1. 如何方便地检测内存泄漏情况呢?

推荐使用MLeaksFinder

这个工具利用AOP的方法去检测ViewController的pop和dismiss情况。

然后在短暂的延迟之后,去查看vc和subviews是否存在来判断该对象是否属于被泄漏的内存。

由于在iOS开发中,大部分的代码都是围绕着VC和View来做事情( UI仔 ?XD ),所以这个检测手段在视图领域是很有效的。

  1. 检测到泄漏之后如何去找到是什么原因造成的泄漏?

推荐使用Xcode’s Memory Graph Debugger

XCode自带的内存快照功能,可以看到当前情况的内存分配和持有情况。当然实际使用下来的话,发现这个工具并不是总准确的,而且里面出现的很多类名是你从来没有见过的私有类。他会给你一些排查问题上的帮助,但并不是一颗银弹。

6. 总结

内存问题没有办法彻底解决,排查的时候也没有百分百命中的途径。

但我们有方法总是能把软件优化的更好。