前段时间被分配查应用的内存泄露问题,然后搜集了一大波内存管理相关的资料,并且在整个过程中狠狠的实践了一波。下面就把这段时间内碰到的坑(从别的地方也搜刮到很多坑),以及搜集到的一些理论知识(简洁版)总结一发。
背景:一个复杂的iOS应用可能会混合着MRR和ARC两种内存管理方式,就是有一部分文件MRR实现,而另一部分则是ARC实现(原因目测是因为复用MRR实现的旧代码)。所以就存在一种现象,如果后来的开发者没注意该文件是否为ARC模式时,则有可能会忘记对对象进行release等操作从而导致内存泄露的发生。
除了项目可能是ARC和MRR混合模式,有时候还有很多底层库是使用C/C++实现的,所以对于C/C++的内存管理也是需要有所了解的。
从苹果的开发者文档里可以看到,一个 app 的内存分三类:
Leaked memory
: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).
Abandoned memory
: Memory still referenced by your application that has no useful purpose.
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
(这个是不是就是传说中的循环引用问题)相关的问题。
在 MRR 模式下 Leaked memory
是比较常见,因为很容易忘了发送 release
消息。而在 ARC 时代更常见的内存泄露是循环引用导致的 Abandoned memory
,Leaks
工具是无法直接检测出这类内存泄露问题的,可以借助一些别的工具(例如静态分析Static Analyze
)。(引用出处【传送门】)
iOS平台内存管理采用的是引用计数机制,主要提供了MRR(Manual Retain-Release)和ARC(Automatic Reference Counting)两种内存管理方式,同时还提供了自动释放池这种半自动释放机制。
PS:这里的MRR就是传说中的MRR。只是我查
iOS Developer
文档时发现好像没有MRR这个叫法,也不知道这个叫法是怎么来的…
无论是MRR还是ARC,其本质都是引用计数。在MRR模式中,通过发送alloc,new, copy等方法可以创建一个对象,刚创建的对象引用计数值都为1。然后通过发送retain方法可以拥有该对象,同时会使得该对象的引用计数+1。最后可以向对象发送release消息放弃该对象所有权,同时会使得该对象的引用计数-1。当一个对象的引用计数为0时,则会被系统回收释放。
ARC实际上是一种编译器特性。编译器会在编译时自动在适当的位置插入retain, release消息来进行对象内存管理。而对于实际开发,只需要谨记:当一个对象被任何强指针引用时,则会被系统回收释放(可以理解为有几个强引用指向对象,则该对象引用计数为x,当没有强指针引用对象,则其引用计数为0)。
我用到的检测内存泄露的工具主要是Xcode中集成的Leaks
组件,这个组件的检测准确率比较高,可以查看到很多比如说是泄露大小,泄露产生的地方及其堆栈信息等。
这里的“泄露产生的地方”并不一定可以定位到具体发生泄漏的某一句代码,而是会标出发生泄漏的对象初始化分配内存的地方,然后需要具体去分析和跟踪该对象的内存管理来查找泄漏的原因。
Allocations
工具是一个跟踪由应用程序分配的对象内存的工具。可以用来在疑似内存泄露的地方,通过反复操作,查看某些对象内存是否有被正常的释放,从而得知是否发生内存泄露。
Analyze
是一款静态分析代码的工具。它可以发现一些逻辑错误,内存泄漏和声明错误(未使用变量)等。这个组件还可以检测出一些内存泄漏问题,比如一些比较明显的循环引用,CF库对象未release等相对简单的问题,通常是在进行其他方式检测之前就使用的方式,把一些简单的问题先发现并处理了。
参考资料:
1. 使用Instruments定位iOS应用的Memory Leaks
2. Leaks Instrument
3. iOS性能优化:Instruments 工具的救命三招
2. IOS性能调优系列:使用Allocation动态分析内存使用情况
1. IOS性能调优系列:Analyze静态分析
2. IPhone开发工具篇-利用xcode profile和analyze进行性能优化
此外还有一些“植入”项目中的内存检测组件,比如说Facebook iOS 内存检测三剑客(FBAllocationTracker/FBMemoryProfiler/FBRetainCycleDetector),MSLeakHunter,MLeaksFinder,PLeakSniffer等等。
这些组件的其中大部分的实现原理都是类似的。主要就是灵活运用了OC中的Rumtime
机制,以及各种OC对象生命周期管理相关的特性。这些组件为了实现对OC对象的内存监控,其本质就是在这些对象被分配和释放的时机进行监测。
比如说需要监测一个UIViewController
类型的对象,就可以联想到iOS中VC的生命周期和UINavigationController
有很大关系,因为后者在iOS应用中常常被用来管理大量VC的跳转控制。所以就可以考虑通过监控UINavigationController
的navigation stack
来达到检测VC是否发生内存泄露的目的。(以下这些方法都会被hook)
再比如说常见的NSObject
对象,其alloc
和dealloc
方法就是对象生命周期中很重要的两个方法,分别是分配内存资源和释放内存资源时会被调用的方法。那么就可以考虑通过method swizzing
方法替换alloc
和dealloc
这两个方法的实现,从而获得对象内存分配和释放的相关信息。
(1)如何判断VC是否还在内存驻留?
答:利用ARC中weak指针指向的对象在对象释放时会自动置为nil的特性来检测VC是否在内存驻留。
(2) 在什么时机检测VC是否发生内存泄露?
答:通过监控UINavigationController
的navigation stack
,可以判断一个VC的生命周期的开始和结束。就是当VC从navigation stack
移除且VC的viewDidDisappear
方法执行时,可以粗略的认为一个VC的生命周期即将结束。这时候就可以创建一个指向该VC的weak指针,并初始化一个定时器对VC进行延时扫描,最后通过(1)中的方法判断VC是否还驻留在内存从而得出VC是否发生内存泄露的结论。
点评:呵呵,在实践中发现最多的问题就是这个。尤其是在ARC和MRR都有的项目中。= =。我猜测原因之一可能是后面的代码修改者没意识到当前修改的文件是MRR模式的,所以在新增一些属性或成员变量后,没有在dealloc方法或对象使用完毕后及时的释放资源。
2. 在MRR模式下,发送了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
Tips:这里存在一个问题就是functionA中对一个对象发送了retain消息,如果这时候又调用了functionB方法,str变量被重新赋值。此时如果没有先对str发送release消息的话,则会导致functionA中引用的对象发生内存泄露。
对于一般情况下使用的局部变量都会记得发送retain后发送release,然而在栗子中那种情况下,成员变量可能在不同方法中被重新赋值的时候,就要注意了!
3. 不论是MRR还是ARC情况下,使用Core Foundation框架(C语言实现的框架,其可以和Cocoa Foundation库中的对象进行类型转换)创建的对象需要手动进行内存管理。即需要手动调用CFRetain和CFRelease来管理对象内存。
Tips:这种情况没啥好说的了,就是记得CFRetain、CFRelease和retain、release一样要成对出现~
再多说一点就是Core Foundation
框架和Cocoa Foundation
对象指针转换的内容。Cocoa Foundation
指针与Core Foundation
指针转换,需要考虑的是所指向对象所有权的归属。ARC提供了3个修饰符来管理。【参考资料: IOS之Core Foundation框架和Cocoa Foundation框架区别 、Core Foundation Framework Reference】
__bridge
,什么也不做,仅仅是转换。此种情况下:
(1). 从Cocoa转换到Core,需要人工CFRetain,否则,Cocoa指针释放后, 传出去的指针则无效。
(2). 从Core转换到Cocoa,需要人工CFRelease,否则,Cocoa指针释放后,对象引用计数仍为1,不会被销毁。
__bridge_retained
,转换后自动调用CFRetain,即帮助自动解决上述(1)的情形。
__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];
}
Tips:栗子中的案例虽然看起来是个很逗比的错误,不过在实战中已经发现两处了…所以如果是MRR方式下这样使用自动释放池时,记得也要对自动释放池发送drain或release操作。如果是使用ARC的话,则不推荐栗子中使用自动释放池的方式,而是下面这种方式了。
@autoreleasepool {
// Code benefitting from a local autorelease pool.
}
既然说到自动释放池,那就顺便简单了解一下其实现原理,使用场景和一些注意事项吧。上面也有提到NSAutoreleasePool有两个方法drain和release,关于这两者的区别可以参考这些资料:【NSAutoReleasePool使用中drain和release的区别】【NSAutoreleasePool】。此外,还发现了一篇讲解AutoReleasePool的比较好的文章,里面也有解释了AutoReleasePool释放时间,原理等等:【黑幕背后的Autorelease】。
5. 函数返回的对象,是否加入自动释放池(延迟释放)。从内存管理的规范上来讲,如果一个函数需要返回一个对象,这个对象应该加入自动释放池中(”谁创建,谁释放”)?虽然说从某种角度来说,不加进自动释放池,而是由函数调用者负责该对象的释放也是可行的。如果函数返回的对象没有加入自动释放池,而函数调用者在外部又没有释放该对象,则就有可能造成内存泄露的现象。
(1)OC中有一些对象有多种创建的方法,比如说NSString, NSArray, NSDictionary之类的(还有它们的可变类型)。这些类都提供了两种类型的创建方式,一种是成员函数initWithXXX,另一种则是类函数stringWithXXX, arrayWithXXX(或array), dictionaryWithXXX(或dictionary)这些。
这些方法都是有区别的,第一种方式产生的对象需要手动release来释放内存,第二种方式产生的对象已经被加到autoreleasepool中,不需要手动release来释放内存。所以在项目中也要注意这些对象使用不同创建方式时所采用的不同的对象管理方法,针对这两种对象生成方式,也有很多讨论,大家自己看看吧哈哈哈哈哈。
参考资料:
1. stringWithFormat vs. initWithFormat on NSString
2. objective-C: NSString应该用initWithFormat? 还是 stringWithFormat?
3.Difference between [NSMutableArray array] vs [[NSMutableArray alloc] init]
(2)其中就碰到过Runtime方法中的class_copyIvarList
,class_copyMethodList
这些方法返回的对象没有被手动释放导致的内存泄漏。因为这些是C实现的函数,是需要手动对函数返回值进行free的,不然则会导致内存泄露。= =。这里也顺便提醒平时需要注意对于C/C++的实现,当见到malloc/new分配的对象,就应该检查该对象有没有对应的free/delete操作,这些地方往往也是内存泄漏产生的地方。
这是无论在MRR还是ARC下都存在的一种导致内存泄露的情况,尤其是在ARC中,如果发生内存泄漏,其一般都会是罪魁祸首。尤其是在使用block的时候,更要注意适当处理以避免强引用循环的发生。
后续有待更新。。。。