I've got a bizarre what-seems-like-a-over-release issue in a tab bar application. My advance apologies for the complexity of this problem description. Hopefully a little example code will help.
I have my application delegate MyAppDelegate
set up as the UITabBarControllerDelegate
:
- (BOOL)tabBarController:(UITabBarController *)tabBarControllerIn shouldSelectViewController:(UIViewController *)viewController {
return YES;
}
- (void)tabBarController:(UITabBarController *)tabBarControllerIn didSelectViewController {
if ([viewController isKindOfClass:[UINavigationController class]]) {
UINavigationController *navController = (UINavigationController *)viewController;
[navController popToRootViewControllerAnimated:NO];
}
}
The tab bar view is set up with a UINavigationController
for each of 5 tabs. The root view controller (call it CrashingViewController
) in the UINavigationController
configured for tab 4 is derived from UIViewController
and conforms to the UITableViewDataSource
and UITableViewDelegate
protocols to support a UITableView
subview, which is just a 4-row table, each cell of which allowing the user to navigate to another view. In -[UITableViewDelegate tableView:didSelectRowAtIndexPath:]
:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
ASubViewController1 *svc1;
ASubViewController2 *svc2; // this one inherits from UITableViewController;
ASubViewController3 *svc3;
ASubViewController4 *svc4;
switch (indexPath.row) {
case 1:
svc1 = [[ASubViewController1 alloc] init];
[self.navigationController pushViewController:svc1 animated:YES];
[svc1 release];
break;
case 2:
svc2 = [[ASubViewController2 alloc] init];
[self.navigationController pushViewController:svc2 animated:YES];
[svc2 release]; // if I comment this out, I avoid the problem described
break;
/* case 3 and 4 are exactly like the previous two */
default:
break;
}
}
What happens is this: if the user selects the 4th tab of the tab bar, the CrashingViewController
's view is displayed. Then, the user taps the second cell in its table, which pushes an instance of ASubViewController2
onto the navigation stack. So far so good. Now, if the user taps another tab on the tab bar, another view controller's view is presented. If the user then taps the 4th tab again, the UINavigationController
configured as the root view controller for that tab pops all its items and is supposed to show its root view controller. It's during this that the app crashes, as the UIKit framework somewhere along the way calls [ASubViewController2 respondsToSelector:] on svc2
after it has already been freed.
The thing that I'm having a hard time with is this: why is this crash occurring when the user navigates to ASubViewController2
, but none of the other ASubViewController*
s? Also, if I fail to release svc2
, the crash doesn't occur - so it's definitely a double release on svc2
somewhere, I just don't know where that's happening. svc2
is a method-local variable and I release it right after allocating it. Does a UITableViewController
somehow act differently when being popped off a navigation controller's stack than does a UIViewController
? Am I missing something else?
I'm sure I'm just missing something that's well-documented or otherwise straight-forward, but I've been looking at it long enough to have some blind spots in place.
update:
I do have NSZombiesEnabled. here's the message given in the console:
*** -[ASubViewController2 respondsToSelector:]: message sent to deallocated instance 0xe345cd0
And the backtrace:
#0 0x02a18fa7 in ___forwarding___ ()
#1 0x02a18e72 in __forwarding_prep_0___ ()
#2 0x003e69be in -[UITableView(UITableViewInternal) _spacingForExtraSeparators] ()
#3 0x003ede94 in -[UITableView(_UITableViewPrivate) _adjustExtraSeparators] ()
#4 0x003e6743 in -[UITableView layoutSubviews] ()
#5 0x027b9481 in -[CALayer layoutSublayers] ()
#6 0x027b91b1 in CALayerLayoutIfNeeded ()
#7 0x027b22e0 in CA::Context::commit_transaction ()
#8 0x027b2040 in CA::Transaction::commit ()
#9 0x027e2ebb in CA::Transaction::observer_callback ()
#10 0x02a88f4b in __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ ()
#11 0x02a1db27 in __CFRunLoopDoObservers ()
#12 0x029e6ce7 in __CFRunLoopRun ()
#13 0x029e6350 in CFRunLoopRunSpecific ()
#14 0x029e6271 in CFRunLoopRunInMode ()
#15 0x0329200c in GSEventRunModal ()
#16 0x032920d1 in GSEventRun ()
#17 0x00380af2 in UIApplicationMain ()
#18 0x0000226a in main (argc=1, argv=0xbfffef64) at ~/main.m:16
update 2:
The dealloc
method of ASubViewController2
is called on svc2
after the user taps tab 4 the second time and before the zombie message is output. None of my code is in the code path for the dealloc.