block块使用不当导致的循环引用与内存泄漏

注:本文所用代码都是在MRC下

问题场景

首先我们来看下面一段代码,思考一下,会出现什么问题?

    UINavigationController *navi = (UINavigationController *)[_mainTabBar selectedViewController];
    id<UINavigationControllerDelegate> navigatVCDelegate = navi.delegate;
    IFundViewController *fundViewController = [[IFundViewController alloc]initWithUserID:strUserid andParams:params];
    [fundViewController setControllerStackBehavior:^{
        navi.delegate = navigatVCDelegate;
        [navi popViewControllerAnimated:YES];
        [[NSNotificationCenter defaultCenter] postNotificationName:NT_VIPUSERLOGIN object:self];
        fundViewController.hidesBottomBarWhenPushed = NO;
        navi.navigationBarHidden = NO;
    }];
    [fundViewController setHidesBottomBarWhenPushed:YES];
    [fundViewController pushSDKUseNavigation:navi];
    navi.delegate = nil;

分析问题

经过观察,我们可以发现这段代码存在以下问题:

  • fundViewController未被释放,出现内存泄漏.
  • block块中出现循环引用.

那么我们就来分析一下这些问题是怎么出现的,以及该怎么解决这个问题

1.内存泄漏

在MRC下,所有的内存都是由编码者自己手动管理,依据的原则就是谁管理谁释放,很明显的,我们看到我们通过alloc创建对象以后,并没有在使用完之后进行release操作,这就会造成内存泄漏.

2.循环引用

fundViewController所持有的block块在使用的过程中会对内部的对象进行持有,也就是说这个block块对fundViewControllernavi两个对象都持有一份.

那这里就会出现两个循环引用:

  • fundViewControllerblock块持有一份,block块又对fundViewController持有一份.也即是block块与fundViewController相互持有.
  • block块对navi持有一份,navifundViewController持有一份,fundViewControllerblock持有一份.也即是block块与navi相互持有.

注:也正是因为这种循环引用的出现,我们即使在使用完fundViewController时进行release操作还是会导致内存泄漏,因为循环引用,对象一直被持有,就会释放不掉.也就是说在上段代码中,我们必须先解决循环引用的问题才能解决内存泄漏的问题.

那怎么解决这个问题呢?既然出现了循环引用,我们肯定是想打破这种循环引用,在MRC下打破这种循环引用就要使用__block(ARC下使用__weak)来修饰block块中使用的对象.这样在block中使用时就不会对外面的变量进行持有,也就是打破了这种循环引用.

解决问题

经过上面的分析,代码改成如下就会解决以上两个问题.

    __block UINavigationController *navi = (UINavigationController *)[_mainTabBar selectedViewController];
    id<UINavigationControllerDelegate> navigatVCDelegate = navi.delegate;
    __block IFundViewController *fundViewController = [[IFundViewController alloc]initWithUserID:strUserid andParams:params];
    [fundViewController setControllerStackBehavior:^{
        navi.delegate = navigatVCDelegate;
        [navi popViewControllerAnimated:YES];
        [[NSNotificationCenter defaultCenter] postNotificationName:NT_VIPUSERLOGIN object:self];
        fundViewController.hidesBottomBarWhenPushed = NO;
        navi.navigationBarHidden = NO;
    }];
    [fundViewController setHidesBottomBarWhenPushed:YES];
    [fundViewController pushSDKUseNavigation:navi];
    navi.delegate = nil;
    [fundViewController release];