我遇到了一个奇怪的问题,我不知道它是我的bug(很可能)还是UINavigationController的bug。
我在我的应用程序中使用UINavigationController。在某些情况下,我需要进行复杂的导航,例如“弹出2个屏幕并推入新屏幕”。目前,我通过获取当前的navigationController.viewControllers
,修改集合,并调用[navigationController setViewControllers:newStack animated:YES]
来完成此操作。
结果发现,这使得我的应用程序经常崩溃。通常崩溃是SIGBUS或SIGSEGV。重现步骤如下:
- 进行其中一种复杂导航
- 根据导航类型向后导航一次或两次
- 崩溃!
有趣的事情是:
- 如果导航只是“几个屏幕之前”,那么下一个后退会崩溃。如果导航是“几个屏幕之前并推入新屏幕”,那么第二个后退会崩溃。此外,在这种情况下,像后退、前进、后退、后退这样的导航通常不会崩溃应用程序
- 最奇怪的是:如果我在更新堆栈之前执行
[navigationController popToRootViewControllerAnimated:NO]
,应用程序就不会崩溃,或者至少崩溃得更少(我无法重现崩溃)。
这是由我的信号处理程序捕获的崩溃堆栈跟踪示例:
- 0x0027a9 mysighandler()
- 0x3293d82b _sigtramp()
- 0x31c59065 -[UIApplication sendAction:to:from:forEvent:]
- 0x31c59005 -[UIApplication sendAction:toTarget:fromSender:forEvent:]
- 0x31c58fd7 -[UIControl sendAction:to:forEvent:]
- 0x31c58d31 -[UIControl _sendActionsForEvents:withEvent:]
- 0x31c59645 -[UIControl touchesEnded:withEvent:]
- 0x31c5865d -[UIWindow _sendTouchesForEvent:]
- 0x31c58039 -[UIWindow sendEvent:]
- 0x31c5492f -[UIApplication sendEvent:]
- 0x31c543a7 _UIApplicationHandleEvent()
- 0x3352c9ed PurpleEventCallback()
- 0x3358ac2d CFRunLoopRunSpecific()
- 0x3358a35d CFRunLoopRunInMode()
- 0x3352bb33 GSEventRunModal()
- 0x3352bbdf GSEventRun()
- 0x31c1976f -[UIApplication _run]
- 0x31c18473 UIApplicationMain()
- 0x00214d main()
- 0x0020c4 start()
另一个:
- 0x002945 mysighandler()
- 0x3293d82b _sigtramp()
- 0x31c5ead3 -[UIScrollView _updatePanWithStartDelta:event:gesture:ignoringDirectionalScroll:]
- 0x31c5e435 -[UIScrollView handlePan:]
- 0x31d14651 -[UITableView handlePan:]
- 0x33590da7 -[Protocol performSelector:withObject:]
- 0x31c428b5 -[UIGestureRecognizer _updateGestureWithEvent:]
- 0x31c427a9 -[UIGestureRecognizer _updateGestureStateWithEvent:afterDelay:]
- 0x31c583d5 -[UIWindow _sendGesturesForEvent:]
- 0x31c5802b -[UIWindow sendEvent:]
- 0x31c5492f -[UIApplication sendEvent:]
- 0x31c543a7 _UIApplicationHandleEvent()
- 0x3352c9ed PurpleEventCallback()
- 0x3358ac2d CFRunLoopRunSpecific()
- 0x3358a35d CFRunLoopRunInMode()
- 0x3352bb33 GSEventRunModal()
- 0x3352bbdf GSEventRun()
- 0x31c1976f -[UIApplication _run]
- 0x31c18473 UIApplicationMain()
- 0x0022e9 main()
- 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:被调用)。