I recently (after hours of debugging) tracked down a segfault in an Objective-C iPad app. To boil it down, I had an object TOP which owned MIDDLE which owned BOTTOM. MIDDLE and BOTTOM had retain counts of 1. MIDDLE passed BOTTOM to a method in TOP which ended up releasing MIDDLE, thereby causing BOTTOM to be released and dealloc’d. When the same method in TOP continued working with BOTTOM, it segfaulted. (Note that there are multiple layers of indirection that I left out of the description to keep it simple, but which made debugging a chore.)
Is there a name for what happened? Is there a pattern I can follow to prevent it from happening in the future? Why doesn’t the runtime retain objects on the callstack by essentially wrapping methods with [self retain] and [self release] (or retaining arguments the same way, or both)?
Edit:
To be clear, when TOP releases MIDDLE, it sets the pointer to nil. MIDDLE is never accessed through an invalid pointer.
Edit 2:
I should have posted actual code to begin with. This is essentially what I have:
// also known as TOP
@interface MyAppDelegate : NSObject <UIApplicationDelegate> {
UIViewController* controller;
}
@end
@implementation MyAppDelegate
- (void)displayDoc:(Document*)doc {
DocController* c = [[DocController alloc] initWithNibName:@"DocController" bundle:nil doc:doc];
[controller release];
// controller was previously an instance of HomeController
controller = c;
}
- (void)displayBookmark:(Bookmark*)bookmark {
[self displayDoc:bookmark.document];
[controller setPage:bookmark.page];
}
- (void)dealloc {
[controller release];
[super dealloc]
}
@end
// also known as MIDDLE
@interface HomeController : UIViewController {
}
@end
@implementation HomeController
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
Bookmark* b = ...; // pull out of existing data structure, not created here
MyAppDelegate* app = ...;
[app displayBookmark:b];
}
@end
// also known as BOTTOM
@interface Bookmark : NSObject {
}
@property NSString* name;
// etc.
@end
There are a number of reasons the runtime can’t reasonably retain and release every argument for you.
People already complain about Objective-C’s inefficiency. Forcing arity*2 messages for every method call in addition to the method’s actual code would be nuts.
An arbitrary object cannot be assumed to respond to
retainandrelease. That’s unique to classes conforming toNSObject.Arguments can be and often are rebound. In order to do this, the runtime would have to track the initial values of the arguments.
There are reasons not to want this behavior, which seem at least as valid as the reasons for wanting it (essentially, it could simplify a design in cases where you don’t want to go to the trouble of following the memory management contract). There could be a method that intentionally releases an argument and allocates a new one in its place (along the lines of an
initmethod) and in the case of large objects, this could result in a much larger memory footprint. This would be especially troublesome on the iPhone, which is the primary platform forretainandrelease.