ARC 下内存泄露的那些点

在网上搜了一下,发现这篇文章是第一篇、也是惟一 一篇总结 ARC 内存泄露的博客,哈哈好兴奋。html

在 iOS 4.2 时,苹果推出了 ARC 的内存管理机制。这是一种编译期的内存管理方式,在编译时,编译器会判断 Cocoa 对象的使用情况,并适当的加上 retain 和 release,使得对象的内存被合理的管理。因此,ARC 和 MRC 在本质上是同样的,都是经过引用计数的内存管理方式。objective-c

然而 ARC 并非万能的,有时为了程序可以正常运行,会隐式的持有或复制对象,若是不加以注意,便会形成内存泄露!今天就列举几个在 ARC 下容易产生内存泄露的点,和各位童鞋一块儿分享下。安全


 

block 系列

在 ARC 下,当 block 获取到外部变量时,因为编译器没法预测获取到的变量什么时候会被忽然释放,为了保证程序可以正确运行,让 block 持有获取到的变量,向系统显明:我要用它,大家千万别把它回收了!然而,也正因 block 持有了变量,容易致使变量和 block 的循环引用,形成内存泄露! 关于 block 的更多内容,请移步《block 没那么难》网络

 
  1. /**
  2. * 本例取自《Effective Objective-C 2.0》
  3. *
  4. * NetworkFetecher 为自定义的网络获取器的类
  5. */
  6. //EOCNetworkFetcher.h
  7. #import <Foundation/Foundation.h>
  8. typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
  9. @interface EOCNetworkFetcher : NSObject
  10. @property (nonatomic, strong, readonly) NSURL *url;
  11. - (id)initWithURL:(NSURL *)url;
  12. - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
  13. @end;
 
  1. //EOCNetworkFetcher.m
  2. #import "EOCNetworkFetcher.h"
  3. @interface EOCNetworkFetcher ()
  4. @property (nonatomic, strong, readwrite) NSURL *url;
  5. @property (nonatomic, copy) (EOCNetworkFetcherCompletionHandler)completionHandler;
  6. @property (nonatomic, strong) NetworkFetecher *networkFetecher;
  7. @end;
  8. @implementation EOCNetworkFetcher
  9. - (id)initWithURL:(NSURL *)url
  10. {
  11. if (self = [super init]) {
  12. _url = url;
  13. }
  14. return self;
  15. }
  16. - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion
  17. {
  18. self.completionHandler = completion;
  19. /**
  20. * do something;
  21. */
  22. }
  23. - (void)p_requestCompleted
  24. {
  25. if (_completionHandler) {
  26. _completionHandler(_downloaderData);
  27. }
  28. }
 
  1. /**
  2. * 某个类可能会建立网络获取器,并用它从 URL 中下载数据
  3. */
  4. @implementation EOCClass {
  5. EOCNetworkFetcher *_networkFetcher;
  6. NSData *_fetcherData;
  7. }
  8. - (void)downloadData
  9. {
  10. NSURL *url = [NSURL alloc] initWithString:@"/* some url string */";
  11. _networkFetcher = [[EOCNetworkFetch alloc] initWithURL:url];
  12. [_networkFetcher startWithCompletionHandler:^(NSData *data) {
  13. NSLog(@"request url %@ finished.", _networkFetcher);
  14. _fetcherData = data;
  15. }]
  16. }
  17. @end;

这个例子的问题就在于在使用 block 的过程当中造成了循环引用:self 持有 networkFetecher;networkFetecher 持有 block;block 持有 self。三者造成循环引用,内存泄露。app

 
  1. // 例2:block 内存泄露
  2. - (void)downloadData
  3. {
  4. NSURL *url = [[NSURL alloc] initWithString:@"/* some url string */"];
  5. NetworkFetecher *networkFetecher = [[NetworkFetecher alloc] initWithURL:url];
  6. [networkFetecher startWithCompletionHandler:^(NSData *data){
  7. NSLog(@"request url: %@", networkFetcher.url);
  8. }];
  9. }

这个例子比上个例子更为隐蔽,networkFetecher 持有 block,block 持有 networkFetecher,造成内存孤岛,没法释放。框架

说到底原来就是循环引用搞的鬼。循环引用的对象是首尾相连,因此只要消除其中一条强引用,其余的对象都会自动释放。对于 block 中的循环引用一般有两种解决方法ide

  • 将对象置为 nil ,消除引用,打破循环引用;
  • 将强引用转换成弱引用,打破循环引用;
 
  1. // 将对象置为 nil ,消除引用,打破循环引用
  2. /*
  3. 这种作法有个很明显的缺点,即开发者必须保证 _networkFetecher = nil; 运行过。若不如此,就没法打破循环引用。
  4. 但这种作法的使用场景也很明显,因为 block 的内存必须等待持有它的对象被置为 nil 后才会释放。因此若是开发者但愿本身控制 block 对象的生命周期时,就可使用这种方法。
  5. */
  6. // 代码中任意地方
  7. _networkFetecher = nil;
  8. - (void)someMethod
  9. {
  10. NSURL *url = [[NSURL alloc] initWithString:@"g.cn"];
  11. _networkFetecher = [[NetworkFetecher alloc] initWithURL:url];
  12. [_networkFetecher startWithCompletionHandler:^(NSData *data){
  13. self.data = data;
  14. }];
  15. }
 
  1. // 将强引用转换成弱引用,打破循环引用
  2. __weak __typeof(self) weakSelf = self;
  3. NSURL *url = [[NSURL alloc] initWithString:@"g.cn"];
  4. _networkFetecher = [[NetworkFetecher alloc] initWithURL:url];
  5. [_networkFetecher startWithCompletionHandler:^(NSData *data){
  6. //若是想防止 weakSelf 被释放,能够再次强引用
  7. __typeof(&*weakSelf) strongSelf = weakSelf;
  8. if (strongSelf)
  9. {
  10. //do something with strongSelf
  11. }
  12. }];

代码 __typeof(&*weakSelf) strongSelf 括号内为何要加 &* 呢?主要是为了兼容早期的 LLVM,更详细的缘由见:Weakself的一种写法性能

block 的内存泄露问题包括自定义的 block,系统框架的 block 如 GCD 等,都须要注意循环引用的问题。测试

有个值得一提的细节是,在种类众多的 block 当中,方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API ,如fetch

- enumerateObjectsUsingBlock:
- sortUsingComparator:

这一类 API 一样会有循环引用的隐患,但缘由并不是编译器作了保留,而是 API 自己会对传入的 block 作一个复制的操做。


 

performSelector 系列

performSelector 顾名思义即在运行时执行一个 selector,最简单的方法以下

- (id)performSelector:(SEL)selector;

这种调用 selector 的方法和直接调用 selector 基本等效,执行效果相同

[object methodName];
[object performSelector:@selector(methodName)];

但 performSelector 相比直接调用更加灵活

 
  1. SEL selector;
  2. if (/* some condition */) {
  3. selector = @selector(newObject);
  4. } else if (/* some other condition */) {
  5. selector = @selector(copy);
  6. } else {
  7. selector = @selector(someProperty);
  8. }
  9. id ret = [object performSelector:selector];

这段代码就至关于在动态之上再动态绑定。在 ARC 下编译这段代码,编译器会发出警告

warning: performSelector may cause a leak because its selector is unknow [-Warc-performSelector-leak]

正是因为动态,编译器不知道即将调用的 selector 是什么,不了解方法签名和返回值,甚至是否有返回值都不懂,因此编译器没法用 ARC 的内存管理规则来判断返回值是否应该释放。所以,ARC 采用了比较谨慎的作法,不添加释放操做,即在方法返回对象时就可能将其持有,从而可能致使内存泄露。

以本段代码为例,前两种状况(newObject, copy)都须要再次释放,而第三种状况不须要。这种泄露隐藏得如此之深,以致于使用 static analyzer 都很难检测到。若是把代码的最后一行改为

[object performSelector:selector];

不建立一个返回值变量测试分析,简直不可思议这里竟然会出现内存问题。因此若是你使用的 selector 有返回值,必定要处理掉。

performSelector 的另外一个可能形成内存泄露的地方在编译器对方法中传入的对象进行保留。听说有位苦命的兄弟曾被此问题搞得欲仙欲死,详情围观 performSelector延时调用致使的内存泄露


 

addObserver 系列

addObserver 即 Objective-C 中的观察者,此系列常见于 NSNotification、KVO 注册通知。注册通知时,为了防止 observer 被忽然释放,形成程序异常,须要持有 observer,这是形成内存泄露的一个隐患之一。

因此为何须要在代码的 dealloc 方法中移除通知,缘由就在于此。

NSNotificationcenter 须要 removeObserver 的缘由是若是不移除的话,被观察者那么还会继续发送消息。若是此时观察者已经释放,消息会转发给其余对象,有可能形成严重的问题《理解消息转发机制》


 

NSTimer

在使用 NSTimer addtarget 时,为了防止 target 被释放而致使的程序异常,timer 会持有 target,因此这也是一处内存泄露的隐患。

 
  1. // NSTimer 内存泄露
  2. /**
  3. * self 持有 timer,timer 在初始化时持有 self,形成循环引用。
  4. * 解决的方法就是使用 invalidate 方法销掉 timer。
  5. */
  6. // interface
  7. @interface SomeViewController : UIViewController
  8. @property (nonatomic, strong) NSTimer *timer;
  9. @end
  10. //implementation
  11. @implementation SomeViewController
  12. - (void)someMethod
  13. {
  14. timer = [NSTimer scheduledTimerWithTimeInterval:0.1
  15. target:self
  16. selector:@selector(handleTimer:)
  17. userInfo:nil
  18. repeats:YES];
  19. }
  20. @end

 

try...catch

作了一年多的 iOS 开发,一开始看到 try...catch 的第一反应是:这什么鬼?怎么历来没听过?确实,try...catch 实在过低调了,固然这也是有缘由的,后面会说。

Apple 提供了 错误处理(NSError)和 异常处理(NSException) 两种机制,而 try...catch 就是使用 exception 捕获异常。NSError 应用在在绝大部分的场景下,而且这也是 Apple 所推荐。那何时用 NSException 呢?在极其严重的直接致使程序崩溃状况下才使用,而且无需考虑恢复问题。水平和经验所限,我也没有使用过 exception,但能够举个系统使用 exception 的例子

 
  1. NSArray *array = @[@"a", @"b", @"c"];
  2. [array objectAtIndex:3];

这小段代码一执行,立刻崩溃,有提示信息

 
  1. 2015-03-08 21:38:02.346 HelloWorldDemo[87324:1024731] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 2]'
  2. *** First throw call stack:
  3. (
  4. /**
  5. * ...
  6. * ...
  7. * 这中间省略了的东西是栈回溯信息,如
  8. *
  9. * 0 CoreFoundation 0x045a0946 __exceptionPreprocess + 182
  10. * 1 libobjc.A.dylib 0x041fba97 objc_exception_throw + 44
  11. * 2 CoreFoundation 0x04483bd2 -[__NSArrayI objectAtIndex:] + 210
  12. * ...
  13. * ...
  14. */
  15. )
  16. libc++abi.dylib: terminating with uncaught exception of type NSException

很熟悉对吧,原来咱们平时看到的各类崩溃提示信息,用的就是 exception。

Objective-C 的 try...catch 的语法格式和 C++/Java 相似,以下

 
  1. @try {
  2. // 可能抛出异常的代码
  3. }
  4. @catch (NSException *exception) {
  5. // 处理异常
  6. }
  7. @finally {
  8. // finally 代码块是可选的
  9. // 但若是写了 finally block,无论有没有异常,block 内的代码都会被执行
  10. }

之前面 NSArray 的越界访问为例,便可写成以下代码

 
  1. NSArray *array = @[@"a", @"b", @"c"];
  2. @try {
  3. // 可能抛出异常的代码
  4. [array objectAtIndex:3];
  5. }
  6. @catch (NSException *exception) {
  7. // 处理异常
  8. NSLog(@"throw an exception: %@", exception.reason);
  9. }
  10. @finally {
  11. NSLog(@"finally execution");
  12. }

使用了 try...catch 后,代码就不会崩溃,执行后打印以下信息

 
  1. 2015-03-08 22:36:34.729 HelloWorldDemo[87590:1066344] throw an exception: *** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 2]
  2. 2015-03-08 22:36:34.729 HelloWorldDemo[87590:1066344] finally execution

那 try...catch 哪里会有内存泄露的隐患呢?咱们先看 MRC 下的状况

 
  1. // MRC 下的 try...catch
  2. // 注意:在 @try @catch @finally 块内定义的变量都是局部变量
  3. @try {
  4. EOCSomeClass *object = [[EOCSomeClass alloc] init];
  5. [object doSomethingMayThrowException];
  6. [object release];
  7. }
  8. @catch (NSException *exception) {
  9. NSLog(@"throw an exception: %@", exception.reason);
  10. }

此处看似正常,但若是 doSomethingMayThrowException 方法抛出了异常,那么 object 对象就没法释放。若是 object 对象持有了重要且稀缺的资源,就可能会形成严重后果。

ARC 的状况会不会好点儿呢?其实更糟糕。咱们觉得 ARC 下,编译器会替咱们作内存释放,其实不会,由于这样须要加入大量的样板代码来跟踪清理对象,从而在抛出异常时将其释放。即便这段代码即便不抛出异常,也会 影响运行期的性能,并且增长进来的额外代码也会增长应用程序的体积,这些反作用都是很明显的。但另外一方面,若是程序都崩溃了,回不回收内存又有什么意义 呢?

因此能够总结下 try...catch 绝迹的缘由:

  • try...catch 设计的目的是用来捕获程序崩溃的状况。
  • 若是为了捕获异常,而在代码中添加 try...catch 和安全处理异常的代码,就会影响性能,增长应用体积。

 

总结

众观全文,ARC 下的内存泄露问题仅仅是因为编译器采用了较为谨慎的策略,为了保证程序可以正常运行,而隐式的复制或持有对象。只要代码多加注意,便可避免不少问题。

相关文章
相关标签/搜索