ios之block循环引用

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

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


block 系列

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

 
/**
 * 本例取自《Effective Objective-C 2.0》
 *
 * NetworkFetecher 为自定义的网络获取器的类
 */ 
//EOCNetworkFetcher.h
#import <Foundation/Foundation.h>
typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
@property (nonatomic, strong, readonly) NSURL *url;
- (id)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
@end;
//EOCNetworkFetcher.m
#import "EOCNetworkFetcher.h"
@interface EOCNetworkFetcher ()
@property (nonatomic, strong, readwrite) NSURL *url;
@property (nonatomic, copy) (EOCNetworkFetcherCompletionHandler)completionHandler;
@property (nonatomic, strong) NetworkFetecher *networkFetecher;
@end;
@implementation EOCNetworkFetcher
- (id)initWithURL:(NSURL *)url
{
    if (self = [super init]) {
        _url = url;
    }
    return self;
}
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion
{
    self.completionHandler = completion;
    /**
     * do something;
     */
}
- (void)p_requestCompleted
{
    if (_completionHandler) {
        _completionHandler(_downloaderData);
    }
}
/**
 * 某个类可能会创建网络获取器,并用它从 URL 中下载数据
 */
@implementation EOCClass {
    EOCNetworkFetcher *_networkFetcher;
    NSData *_fetcherData;
}
- (void)downloadData
{
    NSURL *url = [NSURL alloc] initWithString:@"/* some url string */";
    _networkFetcher = [[EOCNetworkFetch alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData *data) {
        NSLog(@"request url %@ finished.", _networkFetcher);
        _fetcherData = data;
    }]
}
@end;

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

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

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

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

  • 将对象置为 nil ,消除引用,打破循环引用;
  • 将强引用转换成弱引用,打破循环引用;
    // 将对象置为 nil ,消除引用,打破循环引用
    /*
    这种做法有个很明显的缺点,即开发者必须保证 _networkFetecher = nil; 运行过。若不如此,就无法打破循环引用。
    但这种做法的使用场景也很明显,由于 block 的内存必须等待持有它的对象被置为 nil 后才会释放。所以如果开发者希望自己控制 block 对象的生命周期时,就可以使用这种方法。
    */
    // 代码中任意地方
    _networkFetecher = nil;
    - (void)someMethod
    {
        NSURL *url = [[NSURL alloc] initWithString:@"g.cn"];
        _networkFetecher = [[NetworkFetecher alloc] initWithURL:url];
        [_networkFetecher startWithCompletionHandler:^(NSData *data){
            self.data = data;
        }];
    }
    // 将强引用转换成弱引用,打破循环引用
    __weak __typeof(self) weakSelf = self;
    NSURL *url = [[NSURL alloc] initWithString:@"g.cn"];
    _networkFetecher = [[NetworkFetecher alloc] initWithURL:url];
    [_networkFetecher startWithCompletionHandler:^(NSData *data){
        //如果想防止 weakSelf 被释放,可以再次强引用
        __typeof(&*weakSelf) strongSelf = weakSelf;
        if (strongSelf) 
        {
            //do something with strongSelf
        }
    }];

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

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

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

    - 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延时调用导致的内存泄露


NSTimer

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

 

// NSTimer 内存泄露
/**
 * self 持有 timer,timer 在初始化时持有 self,造成循环引用。
 * 解决的方法就是使用 invalidate 方法销掉 timer。
 */
// interface
@interface SomeViewController : UIViewController
@property (nonatomic, strong) NSTimer *timer;
@end
//implementation
@implementation SomeViewController
- (void)someMethod
{
    timer = [NSTimer scheduledTimerWithTimeInterval:0.1  
                                             target:self  
                                           selector:@selector(handleTimer:)  
                                           userInfo:nil  
                                            repeats:YES];  
}
@end

 

总结

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

 

 

posted on 2017-05-19 19:13  liwuking  阅读(266)  评论(0编辑  收藏  举报

导航