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

时间: 2024-08-08 04:38:33

ios之block循环引用的相关文章

iOS中block循环引用问题

1.block是控制器对象的一个属性,则在block内部使用self将会引起循环应用 typedef void(^TestBlock)(); @interface SecondViewController () @property (nonatomic, copy)TestBlock testBlock; @end self.testBlock = ^() { NSLog(@"%@",self.mapView); }; self.testBlock(); 2.把block内部抽出一个作

ios block 循环引用

无意中看到有人在咨询block循环引用如何解决的问题:记录下来,方便童鞋们参考 ios开发中,开了ARC模式,系统自动管理内存,如果程序中用到了block就要注意循环引用带来的内存泄露问题了 这几天遇到一个问题,正常页面dismiss的时候是要调用dealloc方法的,但是我的程序就是不调用,研究了好久终于找到了问题出在哪里了 起初的代码如下: - (void)getMyrelatedShops { [self.loadTimer invalidate]; self.loadTimer = [N

block循环引用的简单说明

- (void)viewDidLoad {     [super viewDidLoad];     // 如果我们不对block进行copy操作, 那么block存在于栈区, 栈区的block块不会对引用的对象进行持有     // 如果我们对block进行了copy操作, 那么block就存在于堆区, block块就会对引用的对象进行持有          Student *student = [[Student alloc] init];     // 如何解决循环引用问题     //

和block循环引用说再见

to be block? or to be delegate? 这是一个钻石恒久远的问题.个人在编码中暂时没有发现两者不能通用的地方,习惯上更偏向于block,没有什么很深刻的原因,只是认为block回调写起来更便捷,直接在上下文中写block回调使得代码结构更清晰,可读性更强.而delegate还需要申明protocol接口,设置代理对象,回调方法与上下文环境不能很好契合,维护起来没有block方便.另外初学者很容易会被忘记设置代理对象坑- 然而惯用block是有代价的,最大的风险就是循环引用

关于block 循环引用 weakSelf

什么是block? 代码块:{}里的东西 block可以想id一样装到array里,dictionary里...但是不能对他发送消息. nsdictionary 里有一个方法:enumerateKeysAndObjectUsingBlock:^(id key,id value,BOOL *stop) 这个方法遍历dictionary里的东西,直到*stop = YES为止. block 里的代码对于主线程里的变量什么的都是可读的.除非主线程里的变量加上__block 例如:__block BOO

iOS Block循环引用

前言 本篇文章精讲iOS开发中使用Block时一定要注意内存管理问题,很容易造成循环引用.本篇文章的目标是帮助大家快速掌握使用block的技巧. 我相信大家都觉得使用block给开发带来了多大的便利,但是有很多开发者对block内存管理掌握得不够好,导致经常出现循环引用的问题.对于新手来说,出现循环引用时,是很难去查找的,因此通过Leaks不一定能检测出来,更重要的还是要靠自己的分析来推断出来. 声景一:Controller之间block传值 现在,我们声明两个控制器类,一个叫ViewContr

iOS Block循环引用精讲

前言 循环引用就是当self 拥有一个block的时候,在block 又调用self的方法.形成你中有我,我中有你,谁都无法将谁释放的困局.又或者解决方法简而言之就一句话的事情:__weak typeof (self) weakSelf = self; 本篇文章精讲iOS开发中使用Block时一定要注意内存管理问题,很容易造成循环引用.本篇文章的目标是帮助大家快速掌握使用block的技巧. 我相信大家都觉得使用block给开发带来了多大的便利,但是有很多开发者对block内存管理掌握得不够好,导

iOS 8:【转】Block循环引用

源地址:http://fann.im/blog/2013/04/17/retain-cycle-in-blocks/ 个人笔记,可能会有理解不够透彻而错误. @fannheyward Objective-C 是基于引用计数(retainCount)来做内存管理,ClassA 用到 ClassB 的时候,通过 alloc/retain/copy 等将 objectB.retainCount+1,不需要的时候通过 release/autorelease 将 objectB.retainCount-1

IOS中的block 循环引用和retain cycle (经典)

retain cycle 的产生 说到retain cycle,首先要提一下Objective-C的内存管理机制. 作为C语言的超集,Objective-C延续了C语言中手动管理内存的方式,但是区别于C++的极其非人道的内存管理,Objective-C提出了一些机制来减少内存管理的难度. 比如:内存计数. 在Objective-C中,凡是继承自NSObject的类都提供了两种方法,retain和release.当我们调用一个对象的retain时,这个对象的内存计数加1,反之,当我们调用relea