这几天闲点,不想撸代码,那就做做内存优化吧!在网上看过几篇博客下面这篇是比较好的 分析了几种内存泄漏。
常用的内存调试技巧,包括以下几种:
dealloc
是否调用查看某个class是否泄露的问题。
具体操作方法:
在往下看之前请下载实例MemoryProblems,我们将以这个工程展开如何检查和解决内存问题。
悬挂指针问题
悬挂指针(Dangling Pointer)就是当指针指向的对象已经释放或回收后,但没有对指针做任何修改(一般来说,将它指向空指针),而是仍然指向原来已经回收的地址。如果指针指向的对象已经释放,但仍然使用,那么就会导致程序crash。
当你运行MemoryProblems后,点击悬挂指针那个选项,就会出现EXC_BAD_ACCESS崩溃信息。
我们看看这个NameListViewController是做什么的?它继承UITableViewController,主要显示多个名字的信息。它的实现文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
要想通过tableView显示数据,首先要实现UITableViewDataSource这个协议,为了瘦身controller和复用data source,我将它分离到一个类ArrayDataSource来实现UITableViewDataSource这个协议。然后在viewDidLoad方法里面将dataSource赋值给tableView.dataSource。
解释完NameListViewController的职责后,接下来我们需要思考出现EXC_BAD_ACCESS错误的原因和位置信息。
一般来说,出现EXC_BAD_ACCESS错误的原因都是悬挂指针导致的,但具体是哪个指针是悬挂指针还不确定,因为控制台并没有给出具体crash信息。
启用NSZombieEnabled
要想得到更多的crash信息,你需要启动NSZombieEnabled。具体步骤如下:
1.选中Edit Scheme,并点击
2.Run -> Diagnostics -> Enable Zombie Objects
设置完之后,再次运行和点击悬挂指针,虽然会再次crash,但这次控制台打印了以下有用信息:
信息message sent to deallocated instance 0x7fe19b081760大意是向一个已释放对象发送信息,也就是已释放对象还调用某个方法。现在我们大概知道什么原因导致程序会crash,但是具体哪个对象被释放还仍然使用呢?
点击上面红色框的Continue program execution按钮继续运行,截图如下:
留意上面的两个红色框,它们两个地址是一样,而且ArrayDataSource前面有个_NSZombie_修饰符,说明dataSource对象被释放还仍然使用。
再进一步看dataSource声明属性的修饰符是assign
1 2 |
|
而assign对应就是__unsafe_unretained,它跟__weak相似,被它修饰的变量都不持有对象的所有权,但当变量指向的对象的RC为0时,变量并不设置为nil,而是继续保存对象的地址。
因此,在viewDidLoad方法中
1 2 3 4 5 6 7 8 |
|
分析完原因和定位错误代码后,至于如何修改,我想大家都心知肚明了,如果还不知道的话,留言给我。
内存泄露问题
还记得上一篇iOS/OS X内存管理(一):基本概念与原理的引用循环例子吗?它会导致内存泄露,上次只是文字描述,不怎么直观,这次我们尝试使用Instruments里面的子工具Leaks来检查内存泄露。
静态分析
一般来说,在程序未运行之前我们可以先通过Clang Static Analyzer(静态分析)来检查代码是否存在bug。比如,内存泄露、文件资源泄露或访问空指针的数据等。下面有个静态分析的例子来讲述如何启用静态分析以及静态分析能够查找哪些bugs。
启动程序后,点击静态分析,马上就出现crash
此时,即使启用NSZombieEnabled,控制台也不能打印出更多有关bug的信息,具体原因是什么,等下会解释。
打开StaticAnalysisViewController,里面引用Facebook Infer工具的代码例子,包含个人日常开发中会出现的bugs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
下面我们通过静态分析来检查代码是否存在bugs。有两个方式:
手动静态分析:每次都是通过点击菜单栏的Product -> Analyze或快捷键shift + command + b
自动静态分析:在Build Settings启用Analyze During 'Build',每次编译时都会自动静态分析
静态分析结果如下:
通过静态分析结果,我们来分析一下为什么NSZombieEnabled不能定位EXC_BAD_ACCESS的错误代码位置。由于callback传入进来的是null指针,而NSZombieEnabled只能针对某个已经释放对象的地址,所以启动NSZombieEnabled是不能定位的,不过可以通过静态分析可得知。
启动Instruments
有时使用静态分析能够检查出一些内存泄露问题,但是有时只有运行时使用Instruments才能检查到,启动Instruments步骤如下:
1.点击Xcode的菜单栏的 Product -> Profile 启动Instruments
2.此时,出现Instruments的工具集,选中Leaks子工具点击
3.打开Leaks工具之后,点击红色圆点按钮启动Leaks工具,在Leaks工具启动同时,模拟器或真机也跟着启动
4.启动Leaks工具后,它会在程序运行时记录内存分配信息和检查是否发生内存泄露。当你点击引用循环进去那个页面后,再返回到主页,就会发生内存泄露
内存泄露.gif
如果发生内存泄露,我们怎么定位哪里发生和为什么会发生内存泄露?
定位内存泄露
借助Leaks能很快定位内存泄露问题,在这个例子中,步骤如下:
首先点击Leak Checks时间条那个红色叉
然后双击某行内存泄露调用栈,会直接跳到内存泄露代码位置
分析内存泄露原因
上面已经定位好内存泄露代码的位置,至于原因是什么?可以查看上一篇的iOS/OS X内存管理(一):基本概念与原理的循环引用例子,那里已经有详细的解释。(将Test里的stong 改为 weak)
难以检测Block引用循环
大多数的内存问题都可以通过静态分析和Instrument Leak工具检测出来,但是有种block引用循环是难以检测的,看我们这个Block内存泄露例子,跟上面的悬挂指针例子差不多,只是在configureCellBlock里面调用一个方法configureCell。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
我们首先用静态分析来看看能不能检查出内存泄露:
结果是没有任何内存泄露的提示,我们再用Instrument Leak工具在运行时看看能不能检查出:
结果跟使用静态分析一样,还是没有任何内存泄露信息的提示。
那么我们怎么知道这个BlockLeakViewController发生了内存泄露呢?还是根据iOS/OS X内存管理机制的一个基本原理:当某个对象的引用计数为0时,它就会自动调用- (void)dealloc方法。
在这个例子中,如果BlockLeakViewController被navigationController pop出去后,没有调用dealloc方法,说明它的某个属性对象仍然被持有,未被释放。而我在dealloc方法打印release BlockLeakViewController信息:
1 2 3 4 |
|
在我点击返回按钮后,其并没有打印出来,因此这个BlockLeakViewController存在内存泄露问题的。至于如何解决block内存泄露这个问题,很多基本功扎实的同学都知道如何解决,不懂的话,自己查资料解决吧!
( __weak typeof(self) wSelf = self; //防止循环引用 将block 里的self 换成 wSelf)
Retain cycle的补充说明:
编译参数设置
为了保证看到代码,而不是一堆无意义的内存地址,参考The .dSYM File in Ios Project进行xcode的设置。
通过Allocation Instrument,我们可以得到内存使用情况。为了清楚地看出是哪部分最可能是内存泄露,可以使用Call Trees视图,然后在右边:
通过以上方法,可以大概确定是哪部分内存泄露。然后看看该class是不是被dealloc
了。
dealloc
了,那不是本文要解决的问题。dealloc
没有调用到,继续往下看。Retain Cycle导致dealloc
没有被调用
在ARC下,dealloc
不能被调用,一般是因为存在Retain Cycle,而导致Retain Cycle的情况可能是下面几种可能(参考iOS Retain Cycle in ARC和Dealloc not being called on ARC app):
1. Blocks
并不是所有在block中引用self
都会带来retain cycle,比如下面的代码就不会有内存泄露:
如果dealloc没有被调用:
1 - (void)testSelfInCocoaBlocks
2 {
3 NSArray *cats = @[@"Smily", @"Garfild", @"Other cat"];
4 [cats enumerateObjectsUsingBlock:^(NSString *cat, NSUInteger idx, BOOL *stop) {
5 [self doSomethingWithCat:cat];
6 }];
7 }
因为在上面的代码中,block ratain了self,但是self中没有retain这个block。只有当block中引用了self,并且self又以某种方式(比如用一个具有strong属性的Property指向该block,或者将该block加入了self的一个具有strong属性的array中)强引用了该block,才会引起内存泄露,比如:
1 - (void)testSelfInBlock
2 {
3 self.block = ^{
4 [self doSomethingWithCat:@"Fat Cat"];
5 };
6 }
有时候即使没有直接引用self,也可能导致self被retain,这叫做“implicit retain”。一种可能的情况就是在block中引用了self的实例变量,比如:
1 - (void)testHiddenSelfInCocoaBlocks
2 {
3 NSArray *cats = @[@"Smily", @"Garfild", @"Other cat"];
4 [cats enumerateObjectsUsingBlock:^(NSString *cat, NSUInteger idx, BOOL *stop) {
5 _aCat = cat;
6 *stop = YES;
7 }];
8 }
这段code在block中引用了self的实例变量_aCat
。
为了避免implicit retain,可以在xcode的build setting中打开implicit retain of ‘self’ within blocks,xcode编译器会给出警告。
2. NSTimer
3. Observers/NSNotificationCenter
如果在view controller中创建了NSTimer,在消失view controller的时候需要调用invalidate
,否则会产生ratain cycle。
当我们在NSNotificationCenter的block中引用self的时候,也会产生retain cycle,比如:
1 [[NSNotificationCenter defaultCenter] addObserverForName:@"not"
2 object:nil
3 queue:[NSOperationQueue mainQueue]
4 usingBlock:^(NSNotification *note) {
5 [self doSomethingWithCat:@"Noty cat"];
6 }];
在不用的时候需要将self从NSNotificationCenter中移除。
4. Array contained reference
5. Delegate
dealloc没被调用的调试
关于ARC下的retainCount
比如在view controller中将self放在了一个array中,而这个array在view controller消失的时候不会被释放,view controller的dealloc
就不会被调用。
delegate的属性应该为weak。
调试dealloc没有被调用的情况,参考Instruments Allocations track alloc and dealloc of objects of user defined classes,可以看到对应实例在整个生命周期中发生的所有和内存有关的事件,包括malloc,ratain,release等和每次事件的call stack。注意其中的两项设置:
在ARC之前,我们可以使用retainCount
得到一个Object被retain的次数。 引入ARC之后,这个方法不能在code中使用,可以使用下面的方法获得retain的次数:
NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)myObject));
或者通过设置断点,在调试窗口输入如下命令:
po object.retainCount
我们什么时候使用retainCount?Never!
总结
一般来说,在创建工程的时候,我都会在Build Settings启用Analyze During 'Build',每次编译时都会自动静态分析。这样的话,写完一小段代码之后,就马上知道是否存在内存泄露或其他bug问题,并且可以修bugs。而在运行过程中,如果出现EXC_BAD_ACCESS,启用NSZombieEnabled,看出现异常后,控制台能否打印出更多的提示信息。如果想在运行时查看是否存在内存泄露,使用Instrument Leak工具。但是有些内存泄露是很难检查出来,有时只有通过手动覆盖dealloc方法,看它最终有没有调用。
补充:Instrument Leak工具
//选择模拟器 或者真机 -------> 选择项目 点击红色的按钮 开始检测
点击选中泄露的红点 选择 call Tree 下面除了hide System Libraies不选 然后双击
跳转到代码出现问题的位置
跳到有内存泄漏的地方,查看代码内存泄漏的原因加以修改。一般出现内存泄漏的原因:僵尸对象,循环引用,block 代理强引用,定时器 kvo 通知没有移除,webView使用不当,多次数循环导致内存暴涨,ViewController不释放 。一般会从这几个面进行排除。
// 大次数循环内存暴涨问题
记得有道比较经典的面试题,查看如下代码有何问题:
1 2 3 4 5 6 |
|
该循环内产生大量的临时对象,直至循环结束才释放,可能导致内存泄漏,解决方法为在循环中创建自己的autoReleasePool,及时释放占用内存大的临时变量,减少内存占用峰值。
1 2 3 4 5 6 7 8 |
|