William

不管走了多远都不要忘记为什么出发

0%

iOS内存泄露原因分析

  去年大致同期就研究过MLeaksFinderFBRetainCycleDetector两个关于iOS内存泄漏的监控工具。前者的思路是:通常一个 UIViewController 在被 pop 之后将会很快被释放,假设在 pop 3 秒钟之后仍然没有被释放,则可以认为这个 UIViewController 存在泄漏的问题。在后续的更新版本中,MLeaksFinder 也依赖了 Facebook 的FBRetainCycleDetector来辅助判断内存泄漏是否是由循环引用引起的。后者的原理是在 Objective-C 中检测循环引用可以抽象为在一个节点为对象,边为对象之间的引用关系的有向无环图(DAG 图)中寻找存在的环。当所有的 Objective-C 对象已经在我们的有向无环图中时,我们所需要做的就是通过深度优先搜索算法来遍历它,并找到循环节点。关于FBRetainCycleDetector的详细解释实践参考该博客《如何在 iOS 中解决循环引用的问题》

内存泄漏和内存溢出

  1. 内存溢出(out of memory): 是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。通俗理解就是内存不够。例如在运行大型软件或游戏时,软件或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。
  2. 内存泄漏(memory leak): 是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,到最后都会被消耗完,产生闪退。

排查内存泄漏

  iOS开发中对内存管理的要求非常严格,一旦存在内存泄漏,很容易导致程序非常容易崩溃。尽管目前iOS开发基本上都是采用的ARC方式进行内存管理,但是稍不注意就会存在内存泄漏的问题。简单的定位内存泄漏的问题可以采用:静态分析方法(Analyze)和动态分析方法(Instrument的leak)。

1.1 静态分析方法

  通过xcode自带的Analyze进行静态内存泄露分析。静态分析方法能发现大部分的问题,但是只能是静态分析结果,还有一些动态分配内存的情形并没有进行分析。所以仅仅使用静态内存泄漏分析得到的结果并不是非常可靠。

1.2 动态内存泄露分析方法

  分析内存泄露不能把所有的内存泄露查出来,有的内存泄露是在运行时,用户操作时才产生的。在Instruments中选择Leaks工具选项,由于leaks是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。选中Leaks Checks,在Details所在栏中选择CallTree,并且在右下角勾选Invert Call Tree 和Hide System Libraries,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方。

1.3 覆盖不完全

  在 MRC 时代 Leaked memory 很常见,因为很容易忘了调用 release,但在 ARC 时代更常见的内存泄露是循环引用导致的 Abandoned memory,Leaks 工具查不出这类内存泄露,应用有限。

WeRead在MLeaksFinder:精准 iOS 内存泄露检测工具中对Allocations做了总结:不断重复 push 和 pop 同一个 UIViewController,理论上来说,push 之前跟 pop 之后,app 会回到相同的状态。因此,在 push 过程中新分配的内存,在 pop 之后应该被 dealloc 掉,除了前几次 push 可能有预热数据和 cache 数据的情况。如果在数次 push 跟 pop 之后,内存还不断增长,则有内存泄露。用这种方法来发现内存泄露还是很不方便的:

  • 首先,你得打开 Allocations
  • 其次,你得一个个场景去重复的操作,无法及时得知泄露,得专门做一遍上述操作,十分繁琐

内存泄露的原因分析

循环引用

  Objective-C 使用引用计数来管理内存与释放未被引用的对象。内存中的对象 A 可以让对象 B 的引用计数加一,即 retain,来使对象 B 尽可能久地存在内存中(只要对象 A 不对它“减一”,即 release)。也就是说:对象 A 持有了对象 B 。
大多数情况下,引用计数这套机制都可以运作得很好。但是,当两个对象直接地,或者更常见的情形是通过某些对象间接地,互相持有了对方,这个时候就陷入了僵局了。这种互相持有对方的引用的现象叫做循环引用。
循环引用会导致一系列的问题。最好的情况是,泄漏的对象本身就会一直长期地占用内存空间,这种情况一般不会造成太大的内存消耗。如果泄漏的对象不停地增加与积累,那么 App 中其他功能模块所能使用的内存就会减少。最坏的情况则是,内存泄漏导致了 App 需要使用的内存超出了限制,这时应用就会闪退了。

UIViewController是否dismiss

  当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。

ViewController中存在NSTimer

  如果你的ViewController中有NSTimer,那么你就要注意了,因为当你调用:

1
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateTime:) userInfo:nil repeats:YES];

上面代码中的target:self就增加了ViewController的return count,如果你不将这个timer invalidate,将别想调用dealloc。

ViewController中的代理delegate

  一个比较隐秘的因素,你去找找与这个类有关的代理,有没有强引用属性。如果你这个VC需要外部传某个Delegate进来,来通过Delegate+protocol的方式传参数给其他对象,那么这个delegate一定不要强引用,尽量assign或者weak,否则你的VC会持续持有这个delegate,直到它自身被释放。

ViewController中Block

  这个可能就是经常容易犯的一个问题了,Block体内使用实例变量也会造成循环引用,使得拥有这个实例的对象不能释放。因为该block本来就是当前viewcontroller的一部分,现在盖子部门又强引用self,导致循环引用无法释放。 例如你这个类叫OneViewController,有个属性是NSString name; 如果你在block体中使用了self.name,或者_name,那样子的话这个类就没法释放。 要解决这个问题其实很简单,就是在block之前申明当前的self引用为弱引用即可。

1
2
3
4
// MRC下代码如下
__block Viewcontroller weakSelf = self;
// ARC下代码如下
__weak Viewcontroller *weakSelf = self;

ViewController的子视图对self的持有

  这个问题也是我们的项目中内存泄漏的问题所在。有时候需要在子视图或者某个cell中点击跳转等操作,需要在子视图或cell中持有当前的ViewController对象,这样跳转之后的back键才能直接返回该页面,同时也不销毁当前ViewController。此时,你就要注意在子视图或者cell中对当前页面的持有对象不能是强引用,尽量assign或者weak,否则会造成循环引用,内存无法释放。

如果对你有帮助,欢迎赞赏。