循环引用的黑科技
问题背景
页面上有一个按钮,点击按钮会推出一个页面,点击按钮会发生概率性崩溃。
相关代码段
下面这段代码是点击页面上按钮执行的代码段,这段代码是在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.解决问题的方案
最开始我是想到两个解决方案
- 将fundViewController作为按钮所在页面的一个属性,这样的话就会延长其生命周期。
- 不在外面执行fundViewController对象的初始化操作,提供一个类方法进行初始化。
针对于这两种方案
- 做成属性会造成fundViewController对象被保留的太久,其实在退出页面的时候这个对象就已经没用了,这里用作临时变量更符合编码规范。所以此方法不可取。
- 这种方法,我是试过的,但是发现结果是和之前没有区别的,看来,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块中就会被循环引用,要想打破这个循环引用有两种办法:
- 将fundViewController置为
nil
。这里的blcok是被fundViewController引用的,所以将其置为空这种方法不行。 - 将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;
}
总结
这里就体现了两个方面知识的灵活运用
- 对循环引用的灵活使用。我们不仅仅是为了防止循环引用,还可以利用循环引用去解决一些问题。
- 如何打破循环引用。