博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
iOS内存管理之超级大坑-内存泄漏
阅读量:4111 次
发布时间:2019-05-25

本文共 5960 字,大约阅读时间需要 19 分钟。

前段时间被分配到查内存泄漏这种大坑,不胜惶恐!!!结果还真的跳进去了,爬了好长一段时间都没爬出来QAQ。每天开着Leaks各种捣鼓爱啪啪,然后看到一大波“神奇”的内存泄露信息,头都大了。

  不过这虽然是个大坑,不过趁着这次机会可以把内存管理知识好好实践了一遍。或许现在大多数的新项目都是ARC的了,然而在一些实际的大项目中,会重用很多诺干年前(其实也就几年前)的代码,QAQ,而这些代码会有很多实现采用的是MRC的方式,所以项目中就经常可以见到混合着ARC和MRC这两种不同内存管理模式的代码。然后ARC文件的代码可能会调用MRC模式实现的代码,当然也还有反过来的情况。

  除了ARC和MRC混合,有时候还有很多底层库是使用C/C++实现的,而且这些库中也涉及一些OC方面的库类调用,这样的情况就又复杂一点了。 = =,所以C/C++的内存管理也是需要有所了解滴~~~

  QAQ,接下来在最后部分会简单列举一些碰到过的发生内存泄漏的情况吧。最后大家会发现这些问题都很简单,但熟话说的好,潜水淹死人啊!

一、检测工具介绍

1.1 Instrument — Leaks,Allocations,Analyze

  我用到的检测内存泄露的工具主要是Xcode中集成的Leaks组件,这个组件的检测准确率还是比较高的(毕竟水果家亲儿子),可以查看到很多比如说是泄露大小,泄露产生的地方及其堆栈信息等。但是这里的“泄露产生的地方”并不一定可以定位到具体发生泄漏的某一句代码,而是会标出发生泄漏的对象初始化分配内存的地方,然后需要具体去分析该对象来查处泄漏的原因。

参考资料: 
1. 【】 
2. 【】

  Allocations工具是一个跟踪由应用程序分配的对象内存的工具。一般就是用来在疑似内存泄露的地方,通过反复操作,查看某些对象内存是否有被正常的释放,从而得知是否发生内存泄露。(= =。这里我并没使用到这个,这算是以前比较古老的检测内存泄漏的方式了,不过某些情况下也还是挺有用。)

参考资料: 
1. 【】 
2. 【】

  Analyze是一款静态分析代码的工具。它可以发现一些逻辑错误,内存泄漏和声明错误(未使用变量)等。这里可以发现的一些内存泄漏问题主要是一些常见的循环引用,CF库对象未release等相对简单的问题,通常是在进行其他方式检测之前就使用的方式,把一些简单的问题先发现并处理了。

参考资料: 
1. 【】 
2. 【】

1.2 内存检测组件

  此外还有一些“植入”项目中的内存检测组件,比如说Facebook iOS 内存检测三剑客(FBAllocationTracker/FBMemoryProfiler/FBRetainCycleDetector),MSLeakHunter,,等等。

  这些组件的实现原理都是大同小异的。主要就是灵活运用了OC中的Rumtime机制,以及各种OC对象生命周期管理相关的特性。这些组件为了实现对OC对象的内存监控,其本质就是在这些对象被分配和释放的时机进行监测,结合系统对这些对象生命周期管理方法实现是否发生内存泄露检测的目的。

  比如说需要监测一个UIViewController类型的对象,就可以联想到iOS中VC的生命周期管理和UINavigationController有很大关系,因为后者在iOS应用者常常被用来管理大量VC的跳转控制。所以就可以考虑通过监控UINavigationController的navigation stack来达到检测VC是否发生内存泄露的目的。(一般以下这些方法都会被hook)

这里写图片描述

  再比如说常见的NSObject对象,其alloc和dealloc方法就是对象生命周期中很重要的两个方法,分别是分配内存资源和释放内存资源时会被调用的方法。然后就可以考虑通过method swizzing方法替换alloc和dealloc这两个方法的实现,这样就可以获得对象内存分配的一些信息。

这里写图片描述

  以下再给出个检测VC内存泄漏的原理:

  1. 如何判断VC是否还在内存驻留? 
    Tips:利用ARC中weak指针指向的对象在对象释放时会自动置为nil的特性来检测VC是否在内存驻留。

  2. 在什么时机检测VC是否发生内存泄露? 
    Tips:通过监控UINavigationController的navigation stack,可以判断一个VC的生命周期的开始和结束。就是当VC从navigation stack移除且VC的viewDidDisappear方法执行时,可以认为一个VC的生命周期即将结束。这时候就可以创建一个指向该VC的weak指针,并初始化一个定时器对VC进行延时扫描,最后通过1中的方法判断VC是否还驻留在内存从而得出VC是否发生内存泄露的结论。


二、应用内存

从苹果的开发者文档里可以看到,一个 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 和 Abandoned memory 都属于应该释放而没释放的内存,都是内存泄露,而 Leaks 工具只负责检测 Leaked memory,而不管 Abandoned memory。在 MRC 时代 Leaked memory 很常见,因为很容易忘了调用 release,但在 ARC 时代更常见的内存泄露是循环引用导致的 Abandoned memory,Leaks 工具查不出这类内存泄露,应用有限。(引用出处【】)


三、实践

3.1 对象内存管理


1. 在MRC模式下,通过new, copy, alloc方式创建的对象,记得release。一般在delloc中进行释放操作。当然局部内产生的也要在局部内进行释放。

点评:呵呵,在实践中发现最多的问题就是这个。尤其是在ARC和MRC都有的项目中。= =。我猜测原因之一可能是后面的代码修改者没意识到当前修改的文件是MRC模式的,所以在新增一些属性或成员变量后,没有在dealloc方法或对象使用完毕后及时的释放资源。


2. 在MRC模式下,发送了retain消息,记得也要发送release消息。并且在一个对象发送retain消息之前,也要考虑是否要release原来的对象。

碰到的一个栗子:

@interface classA{    NSString *_str;}- (void)functionA{    //正确的方式是这里要有: [_str release];    _str = [[NSString stringWithFormat:@"%d", @(213)] retain];    //后续代码...}- (void)functionB{    //正确的方式是这里要有: [_str release];    _str = [[NSString stringWithFormat:@"%d", @(213)] retain];//后续代码...}@end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

Tips:这里存在一个问题就是functionA中对一个对象发送了retain消息,如果这时候又调用了functionB方法,str变量被重新赋值。此时如果没有先对str发送release消息的话,则会导致functionA中引用的对象发生内存泄露。 
对于一般情况下使用的局部变量都会记得发送retain后发送release,然而在栗子中那种情况下,成员变量可能在不同方法中被重新赋值的时候,就要注意了!


3. 不论是MRC还是ARC情况下,使用Core Foundation框架(C语言实现的框架,其可以和Cocoa Foundation库中的对象进行类型转换)创建的对象需要手动进行内存管理。即需要手动调用CFRetain和CFRelease来管理对象内存。

Tips:这种情况没啥好说的了,就是记得CFRetain、CFRelease和retain、release一样要成对出现~

  再多说一点就是Core Foundation框架和Cocoa Foundation对象指针转换的内容。Cocoa Foundation指针与Core Foundation指针转换,需要考虑的是所指向对象所有权的归属。ARC提供了3个修饰符来管理。【参考资料:、】

  1. __bridge,什么也不做,仅仅是转换。此种情况下: 
    (1). 从Cocoa转换到Core,需要人工CFRetain,否则,Cocoa指针释放后, 传出去的指针则无效。

    (2). 从Core转换到Cocoa,需要人工CFRelease,否则,Cocoa指针释放后,对象引用计数仍为1,不会被销毁。

  2. __bridge_retained,转换后自动调用CFRetain,即帮助自动解决上述(1)的情形。

  3. __bridge_transfer,转换后自动调用CFRelease,即帮助自动解决上述(2)的情形。


4. 使用NSAutoreleasePool创建的自动释放池,一定要确保其发送drain或release消息。这样创建的自动释放池对象才会被释放,同时被加入自动释放池的对象才能收到release消息,避免内存泄露。

碰到的栗子:

- (void)functionA{    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];    NSString *str = [[NSString alloc] initWithFormat:@"%d", @(213)];    [str release];    //执行各种代码...    if (...){        //执行各种代码...        //问题就在这里:return 之前没有释放自动释放池!!!        //正确的做法,加上: [pool release];        return;    }    [pool release];}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

Tips:栗子中的案例虽然看起来是个很逗比的错误,不过在实战中已经发现两处了…所以如果是MRC方式下这样使用自动释放池时,记得也要对自动释放池发送drain或release操作。如果是使用ARC的话,则不推荐栗子中使用自动释放池的方式,而是下面这种方式了。

@autoreleasepool {    // Code benefitting from a local autorelease pool.}
  • 1
  • 2
  • 3

  既然说到自动释放池,那就顺便简单了解一下其实现原理,使用场景和一些注意事项吧。上面也有提到NSAutoreleasePool有两个方法drain和release,关于这两者的区别可以参考这些资料:【】【】。此外,还发现了一篇讲解AutoReleasePool的比较好的文章,里面也有解释了AutoReleasePool释放时间,原理等等:【】。


5. 函数返回的对象,是否加入自动释放池(延迟释放)。从内存管理的规范上来讲,如果一个函数需要返回一个对象,这个对象应该加入自动释放池中(”谁创建,谁释放”)?虽然说从某种角度来说,不加进自动释放池,而是由函数调用者负责该对象的释放也是可行的。如果函数返回的对象没有加入自动释放池,而函数调用者在外部又没有释放该对象,则就有可能造成内存泄露的现象。

(1)OC中有一些对象有多种创建的方法,比如说NSString, NSArray, NSDictionary之类的(还有它们的可变类型)。这些类都提供了两种类型的创建方式,一种是成员函数initWithXXX,另一种则是类函数stringWithXXX, arrayWithXXX(或array), dictionaryWithXXX(或dictionary)这些。 
这些方法都是有区别的,第一种方式产生的对象需要手动release来释放内存,第二种方式产生的对象已经被加到autoreleasepool中,不需要手动release来释放内存。所以在项目中也要注意这些对象使用不同创建方式时所采用的不同的对象管理方法,针对这两种对象生成方式,也有很多讨论,大家自己看看吧哈哈哈哈哈。

参考资料: 
1.  
2.  
3.

(2)其中就碰到过Runtime方法中的class_copyIvarListclass_copyMethodList这些方法返回的对象没有被手动释放导致的内存泄漏。因为这些是C实现的函数,是需要手动对函数返回值进行free的,不然则会导致内存泄露。= =。这里也顺便提醒平时需要注意对于C/C++的实现,当见到malloc/new分配的对象,就应该检查该对象有没有对应的free/delete操作,这些地方往往也是内存泄漏产生的地方。


3.2 引用循环

  这是无论在MRC还是ARC下都存在的一种导致内存泄露的情况,尤其是在ARC中,如果发生内存泄漏,其一般都会是罪魁祸首。= =。而且个人觉得引用循环这种问题是最难发现和分析的!项目很大的时候,模块间会很复杂,相互间依赖就很多,一不小心就很容易产生强引用循环这种现象。尤其是在使用到block的时候,更要注意适当处理以避免强引用循环的发生。

你可能感兴趣的文章
SQL如何取日期中的年月
查看>>
C# goto
查看>>
Confluence 6 给一个从 Jira Service Desk 的非许可证用户访问权限
查看>>
node.js基础 1之简单的nodejs模块
查看>>
Cocos2d-x学习笔记(一) 搭建开发环境
查看>>
关于 古人劝学 --写的真心是好 真的有收获
查看>>
【习题 7-7 UVA-12558】Egyptian Fractions (HARD version)
查看>>
【codeforces 768F】Barrels and boxes
查看>>
【66.47%】【codeforces 556B】Case of Fake Numbers
查看>>
Sql 列转行字符串
查看>>
[天下小黑盒]打地鼠小助手
查看>>
input中只允许输入数字
查看>>
dataFrame 切片操作
查看>>
vs2015安装及初步试用
查看>>
C言语教程第五章:函数(10)
查看>>
MySQL中批改暗码及拜访限定设置详解-1
查看>>
Oracle数据垄断和节制言语详解-3
查看>>
投靠Linux第一步 Windows数据向Linux迁徙(3)
查看>>
今晚装了个红旗LINUX6.0系统
查看>>
cGmail — 主动反省邮件
查看>>