UINavigationController的setViewControllers方法导致应用程序崩溃。

4

我遇到了一个奇怪的问题,我不知道它是我的bug(很可能)还是UINavigationController的bug。

我在我的应用程序中使用UINavigationController。在某些情况下,我需要进行复杂的导航,例如“弹出2个屏幕并推入新屏幕”。目前,我通过获取当前的navigationController.viewControllers,修改集合,并调用[navigationController setViewControllers:newStack animated:YES]来完成此操作。

结果发现,这使得我的应用程序经常崩溃。通常崩溃是SIGBUS或SIGSEGV。重现步骤如下:

  1. 进行其中一种复杂导航
  2. 根据导航类型向后导航一次或两次
  3. 崩溃!

有趣的事情是:

  • 如果导航只是“几个屏幕之前”,那么下一个后退会崩溃。如果导航是“几个屏幕之前并推入新屏幕”,那么第二个后退会崩溃。此外,在这种情况下,像后退、前进、后退、后退这样的导航通常不会崩溃应用程序
  • 最奇怪的是:如果我在更新堆栈之前执行[navigationController popToRootViewControllerAnimated:NO],应用程序就不会崩溃,或者至少崩溃得更少(我无法重现崩溃)。

这是由我的信号处理程序捕获的崩溃堆栈跟踪示例:

  1. 0x0027a9 mysighandler()
  2. 0x3293d82b _sigtramp()
  3. 0x31c59065 -[UIApplication sendAction:to:from:forEvent:]
  4. 0x31c59005 -[UIApplication sendAction:toTarget:fromSender:forEvent:]
  5. 0x31c58fd7 -[UIControl sendAction:to:forEvent:]
  6. 0x31c58d31 -[UIControl _sendActionsForEvents:withEvent:]
  7. 0x31c59645 -[UIControl touchesEnded:withEvent:]
  8. 0x31c5865d -[UIWindow _sendTouchesForEvent:]
  9. 0x31c58039 -[UIWindow sendEvent:]
  10. 0x31c5492f -[UIApplication sendEvent:]
  11. 0x31c543a7 _UIApplicationHandleEvent()
  12. 0x3352c9ed PurpleEventCallback()
  13. 0x3358ac2d CFRunLoopRunSpecific()
  14. 0x3358a35d CFRunLoopRunInMode()
  15. 0x3352bb33 GSEventRunModal()
  16. 0x3352bbdf GSEventRun()
  17. 0x31c1976f -[UIApplication _run]
  18. 0x31c18473 UIApplicationMain()
  19. 0x00214d main()
  20. 0x0020c4 start()

另一个:

  1. 0x002945 mysighandler()
  2. 0x3293d82b _sigtramp()
  3. 0x31c5ead3 -[UIScrollView _updatePanWithStartDelta:event:gesture:ignoringDirectionalScroll:]
  4. 0x31c5e435 -[UIScrollView handlePan:]
  5. 0x31d14651 -[UITableView handlePan:]
  6. 0x33590da7 -[Protocol performSelector:withObject:]
  7. 0x31c428b5 -[UIGestureRecognizer _updateGestureWithEvent:]
  8. 0x31c427a9 -[UIGestureRecognizer _updateGestureStateWithEvent:afterDelay:]
  9. 0x31c583d5 -[UIWindow _sendGesturesForEvent:]
  10. 0x31c5802b -[UIWindow sendEvent:]
  11. 0x31c5492f -[UIApplication sendEvent:]
  12. 0x31c543a7 _UIApplicationHandleEvent()
  13. 0x3352c9ed PurpleEventCallback()
  14. 0x3358ac2d CFRunLoopRunSpecific()
  15. 0x3358a35d CFRunLoopRunInMode()
  16. 0x3352bb33 GSEventRunModal()
  17. 0x3352bbdf GSEventRun()
  18. 0x31c1976f -[UIApplication _run]
  19. 0x31c18473 UIApplicationMain()
  20. 0x0022e9 main()
  21. 0x002260 start()

这是一个“复杂”的导航实现示例:

@implementation UINavigationController(MyCategory)
- (void)popViewControllers:(NSInteger)count {
    NSArray* oldList = self.viewControllers;
    NSMutableArray* newList = [NSMutableArray arrayWithArray:oldList];
    if(count > [oldList count]) {
        CLogError(@"Poping %d screens when there is only %d", count, [oldList count]);
        count = [oldList count] - 1;
    }
    for(int i = 0; i<count; i++) {
        [newList removeLastObject];
    }
    [self setViewControllers:newList animated:YES];
}
@end

有人知道我做错了什么吗?我已经没有任何想法了。
补充: 我使用NSZombieEnabled和MallocStackLogging运行我的应用程序,以查找失败的对象。但它并没有给出合理的结果。对于堆栈跟踪#1,它在步骤3(- [UIApplication sendAction:to:from:forEvent:])失败,并且僵尸对象是-[UIBarButtonItem performSelector:withObject:withObject:]:消息发送到已释放实例0xa5f5f90。这是屏幕的正确导航栏按钮,应用程序从该屏幕导航2个屏幕回来(请记住,这2个屏幕后退导航可以正常工作,只有下一个“通常”后退导航失败)。但我不会对该按钮执行任何操作。ViewControler的initWithSomething:(Something*)something中相应的代码如下:
UIBarButtonItem* doneItem = [[UIBarButtonItem alloc] initWithTitle:@"Complete"
                                                             style:UIBarButtonItemStyleDone 
                                                             target:self action:@selector(onDone)];
self.navigationItem.rightBarButtonItem = doneItem;
[doneItem release]; 

这个按钮唯一特别的是onDone选择器会导航到2个屏幕之前,但我不认为这真的重要。所以我相信问题出在更高级别的对象上(可能是视图控制器或UINavigationController?)。但是问题出在哪里呢? 2011年10月4日更新: 由于人们仍然有时在搜索此问题,因此这里提供一些代码。我目前解决这个问题的方法是使用UINavigationController的自定义子类,而不是使用以下hack的UINavigationController(不能保证此方法有效或仍然需要):
@interface CustomUINavigationController : UINavigationController {
}

@end


@implementation CustomUINavigationController
- (void)setViewControllers:(NSArray*)newStack animated:(BOOL)animated { 
    // HACK HACK
    // Somehow everything fails if I don't clean stack before putting new
    // But on iOS4 popToRootViewControllerAnimated might call setViewControllers:animated
    // let's avoid call stack overflow
    static int stackCount = 0;
    if(!stackCount++) {
        if([self.viewControllers count] != 1) {
            [self popToRootViewControllerAnimated:NO];
        }
        else {
            UIViewController* tmpVc = [[[UIViewController alloc] init] autorelease];
            NSArray* tmpStack = [NSArray arrayWithObject:tmpVc];
            [super setViewControllers:tmpStack animated:NO];                
        }
    }
    [super setViewControllers:newStack animated:animated];
    stackCount--;
}
@end

另一个重要的事情是:在上一个动画导航仍在进行中时最好不要开始新的动画导航(即至少要等到viewWillAppear:被调用)。


我认为这个问题类似于:http://stackoverflow.com/a/12076199/980903 - lyzkov
3个回答

2

我认为将其在僵尸模式下通过Instruments运行是值得的。这几乎肯定是一个内存问题,有些东西正在访问已经释放的对象。


抱歉我在原文中没有提到这一点。我尝试了僵尸模式,并在问题中添加了细节。 - SergGr

1
对于第二个堆栈跟踪,我遇到了同样的问题。我释放了包含滚动视图的视图控制器。
[mainScrollView addSubview:rubriqueController.view];        
[rubriqueController release]; // Comment this line

像这样的东西。

希望即使过了9个月,它仍然有所帮助。


不,这似乎不是我的情况。我最终做的是在我的类别中添加自定义的 safeSetViewControllers: 方法,其中包含一些内部技巧,例如弹回到根视图和一些导航栏刷新的强制执行。 - SergGr

1

遇到了类似的问题。事实证明,在某些情况下,当视图控制器被弹出时,rightBarButtonItem 的操作会被调用。我的丑陋解决方法是在将下一个视图控制器推入堆栈时删除有问题的项。然后我在 viewWillAppear 中检查 rightBarButton 是否为空,并在必要时重新创建按钮。

但是,重新创建按钮会使它在导航栏右侧的位置上停留一小段时间,然后才弹出到正确的位置。稍微更优雅的解决方法是将按钮的操作设置为“NULL”的默认值。这解决了第一个问题,但也会破坏 backBarButtonItem。

因此,总结一下,我仍在寻找一个适当的解决方法。我距离联系苹果还有一天的工作时间——想想可能是个 bug...


1
在进一步尝试之后,我通过使用setRightBarButtonItem:animated:来解决了由于“修复”引入的问题,而不是直接设置rightBarButtonItem。不过,我仍在寻找解决原始问题的方法... - Royen
正如我所说,似乎我已经找到了解决我的问题的方法。我使用了自定义的UINavigationController子类,并重写了setViewControllers:animated:方法,在调用super之前调用[self popToRootViewControllerAnimated:NO]。但我不喜欢这种方法,因为我不明白它为什么有效,以及为什么我的代码会失败。这可能是我代码中某些更深层次问题的标志。 - SergGr
你报告过这个 bug 吗?我在 iOS 4 上仍然遇到这个问题。 - an0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接