循环引用的黑科技

问题背景

页面上有一个按钮,点击按钮会推出一个页面,点击按钮会发生概率性崩溃。

相关代码段

下面这段代码是点击页面上按钮执行的代码段,这段代码是在MRC下的:

    __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];
    //push出控制器
    [fundViewController pushSDKUseNavigation:navi];
    navi.delegate = nil;
    [fundViewController release];

下面的代码推出来的页面执行的代码,是在ARC下的:

//这个方法提供对外接口,执行push的方法
- (void)pushSDKUseNavigation:(UINavigationController *)navigationController {
    __weak IFundViewController *weakSelf = self;
    //发一个请求用来获取用户的custid
    [LoginSever requestToGetCustidBy:_userID withBlock:^(NSDictionary *data, NSError *error) {
        if (error == nil) {
            if ([data[@"custid"] length] > 0) {
                NSString *custid = data[@"custid"];
                //这个请求是用来发起自动登录的请求
                [LoginSever sdkAutoLogin:custid withBlock:^(NSDictionary *dic, NSError *error) {
                    [weakSelf pushSDKControllerUseNavi:navigationController];
                }];
            } else {
                [weakSelf pushSDKControllerUseNavi:navigationController];
            }
        } else {
            [weakSelf pushSDKControllerUseNavi:navigationController];
        }
    }];
}

//这个方法是最终推出控制器的方法,无对外接口
- (void)pushSDKControllerUseNavi:(UINavigationController *)navigationController {
    [navigationController pushViewController:self animated:NO];
}

//typedef void (^ControllerStackBehavior)(void);
//@property (copy, nonatomic) ControllerStackBehavior stackBlock;
//这个方法是在页面里面点击左上角的返回按钮执行的方法
- (void)backItemAction {
    //这个blcok块的执行是退出sdk以后在手炒代码里面的处理
    if (_stackBlock) {
        _stackBlock();
    }
}

解决思路

1.找到问题的根本原因

首先崩溃问题要是能在模拟器上复现,那就好办多了。这个问题在模拟器上是可以复现的。

经过打断点发现当该请求requestToGetCustidBy回来的时候,IFundViewController就已经走了dealloc方法,这也是导致应用崩溃的原因。发现了问题,为什么会造成这样的情况,怎么解决呢?

2.分析出现该问题的原因

可以看到点击按钮里面执行pushSDKUseNavigation之后进行了[fundViewController release];的操作,但是其实并不是执行了pushSDKUseNavigation方法之后,fundViewController对象就被压到了堆栈里面,它是在发了两个请求之后才被压到堆栈里面,也就是这个时候才被其他对象持有,经过了这么久,fundViewController肯定早就被释放掉了,也就是还没来得及push就被释放掉了。

3.解决问题的方案

最开始我是想到两个解决方案

  1. 将fundViewController作为按钮所在页面的一个属性,这样的话就会延长其生命周期。
  2. 不在外面执行fundViewController对象的初始化操作,提供一个类方法进行初始化。

针对于这两种方案

  1. 做成属性会造成fundViewController对象被保留的太久,其实在退出页面的时候这个对象就已经没用了,这里用作临时变量更符合编码规范。所以此方法不可取。
  2. 这种方法,我是试过的,但是发现结果是和之前没有区别的,看来,ARC和MRC苹果底层的处理基本是一致的。

后来组长提供了一种解决方案:
其实我们的目的很清楚,在执行[fundViewController release];的时候fundViewController不能被销毁掉,因为这个时候还没push。当退出sdk的时候fundViewController要被销毁掉。

__block IFundViewController *fundViewController = [[IFundViewController alloc]initWithUserID:strUserid andParams:params];

这句代码中的 __block是为了防止循环引用,这里呢,我们把这个给去掉,也就是改成下面这样

IFundViewController *fundViewController = [[IFundViewController alloc]initWithUserID:strUserid andParams:params];

这时候呢,fundViewController在block块中就会被循环引用,要想打破这个循环引用有两种办法:

  1. 将fundViewController置为nil。这里的blcok是被fundViewController引用的,所以将其置为空这种方法不行。
  2. 将block块置为nil

解决办法

这里我们选用的就是在push之前,故意让fundViewController出现循环引用

IFundViewController *fundViewController = [[IFundViewController alloc]initWithUserID:strUserid andParams:params];

这样可以延长其生命周期,当退出页面的时候,调用完block块之后再将blcok块置为nil,打破fundViewController的循环引用,也保证了在退出页面的时候fundViewController是会被销毁掉的。

- (void)backItemAction {
    //这个blcok块的执行是退出sdk以后在手炒代码里面的处理
    if (_stackBlock) {
        _stackBlock();
    }
    //置空block
    _stackBlock = nil;
}

总结

这里就体现了两个方面知识的灵活运用

  1. 对循环引用的灵活使用。我们不仅仅是为了防止循环引用,还可以利用循环引用去解决一些问题。
  2. 如何打破循环引用。