当UIActionSheet遇上Block

众所周知,Block会对其中使用的对象进行强引用,但如果要在Block中要实现弹出ActionSheet的操作,又会有哪些问题呢?

换句话说,当有点像代理的Block遇上真正的代理方法——ActionSheet的点击事件,又会不会产生冲突呢?

1 问题描述

页面是一个TableViewController,由继承自UITableViewCell的不同类作为每个Cell的类,而Cell中的各种元素都交给了模型管理,也包括点击Cell触发的事件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void)addCellGroup
{
OAProfileArrowItem *exit = [OAProfileArrowItem itemWithIcon:@"exit" title:@"退出登录"];
__weak OAProfileViewController *vc = self;

// Cell的option将在didSelectRowAtIndexPath...方法中执行
exit.option = ^{
UIActionSheet *actionLogOut = [[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:@"取消" destructiveButtonTitle:@"退出登录" otherButtonTitles:@"关闭程序", nil];
[actionLogOut showInView:vc.view];
};

// 将Cell对象加到CellGroup中,再交给dataList作为tableView的数据源
OAProfileGroup *group = [[OAProfileGroup alloc] init];
group.items = @[exit];
[self.dataList addObject:group];
}

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 0) {
NSLog(@"%d",0);
UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]];
OALoginViewController *loginVc = (OALoginViewController *)[storyBoard instantiateViewControllerWithIdentifier:@"loginVc"];
UIWindow *window = [UIApplication sharedApplication].keyWindow;
window.rootViewController = loginVc;
}
else if (buttonIndex == 1) {
NSLog(@"%d",1);
}
else if (buttonIndex == 2) {
NSLog(@"%d",2);
}
}

机智的我一看到Block中用到了self.view就意识到这是一个循环引用问题,将会导致页面无法release,于是赶紧在Block外面声明了一个__weak弱指针变量。但是感觉还是有些不对劲:ActionSheet作为Block的内部成员变量,能触发下面的按钮点击事件吗?
先运行试试。。。

Block Bug

为什么刚跳过去就又跳了回来,可是这段跳转控制器的代码明明是没问题的啊,难道是因为跳转之后Block被回收导致代理中声明的OALoginViewController被同时Release了?可是这个控制器明明已经被window强引用了啊?

2 分析

于是我决定将ActionSheet对象的声明放到Block的外面,循环引用什么的先扔到一边吧,然而问题依旧。我决定仔细分析一下引用关系:
当页面跳转之后,虽然Block引用着ActionSheet,但由于ActionSheet本身是局部变量,会解除self.view对它的引用,所以当Block被self解除引用时,Block和ActionSheet同时Release了,而此时OALoginViewController被window引用,所以并不会被Release。

3 解决

所以问题一定出在ActionSheet的点击事件上,我尝试把页面的跳转方式改为modal,发现页面跳转正常了。但这不是我想要的,因为modal并不会释放原控制器。于是我又把点击事件里的方法剪切到actionSheet:didDismissWithButtonIndex:方法,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
- (void)addCellGroup
{
OAProfileArrowItem *exit = [OAProfileArrowItem itemWithIcon:@"exit" title:@"退出登录"];
UIActionSheet *actionLogOut = [[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:@"取消" destructiveButtonTitle:@"退出登录" otherButtonTitles:@"关闭程序", nil];
__weak OAProfileViewController *vc = self;

// Cell的option将在didSelectRowAtIndexPath...方法中执行
exit.option = ^{
[actionLogOut showInView:vc.view];
};

// 将Cell对象加到CellGroup中,再交给dataList作为tableView的数据源
OAProfileGroup *group = [[OAProfileGroup alloc] init];
group.items = @[exit];
[self.dataList addObject:group];
}

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 0) {
NSLog(@"%d",0);
}
else if (buttonIndex == 1) {
NSLog(@"%d",1);
}
else if (buttonIndex == 2) {
NSLog(@"%d",2);
}
}

- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex
{
UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]];
OALoginViewController *loginVc = (OALoginViewController *)[storyBoard instantiateViewControllerWithIdentifier:@"loginVc"];
// [self presentViewController:loginVc animated:YES completion:nil];
UIWindow *window = [UIApplication sharedApplication].keyWindow;
window.rootViewController = loginVc;
}

4 结论

我认为是由于ActionSheet是modal方式覆盖到view上的,而当点击按钮,ActionSheet要消失却在当前Window找不到自身时,就会跳回原来的控制器,所以跳转必须在ActionSheet消失之后进行。
另外原页面的Dealloc方法会在跳转后调用,也达到了释放原页面的目的。

5 后记

本着“不作会死”的态度-_-|||,我又把ActionSheet的声明移到了Block里面,页面跳转还是正常的,但跳转之后原页面没有Release!!!
这又是为什么呢?我的猜测是当执行完Block本应被Release,但ActionSheet被self.view强引用,而ActionSheet与Block互相引用,最终导致页面无法被释放。
这个猜测未经证实,待我找到ARC下查看对象引用计数的方法再回来填坑