Search code examples
iphonecocoa-touchmemory-managementios4uiviewcontroller

EXC_BAD_ACCESS - How is this code causing a leak? Traced via malloc_history & NSZombieEnabled


UPDATE (at bottom): Added entire viewWillAppear: method implementation to show how the three UIViewControllers are allocated / deallocated into the UIScrollView.

I have a root ViewController which contains a UIScrollView. This UIScrollView itself contains 3 different UIViewControllers (setup as NSFetchedResultControllerDelegates for separate UITableViews.)

I need to reload the root ViewController's UIScrollView at one point when we come back to the view so I do this at the beginning of:

-(void) viewWillAppear:(BOOL)animated {
 [dataViewScroller.subviews 
                 makeObjectsPerformSelector:@selector(removeFromSuperview)];
 .......
 // and we proceed to re-load the 3 UIViewControllers into the UIScrollView.
 // From testing this all seems to work fine.
 // The **problem** is that that as soon as this method ends - I get an
 // EXC_BAD_ACCESS error described above.
 // If the above line is removed, everythign works fine.
}

Here is the viewDidLoad method for the each of the UIViewControllers loaded into the UIScrollView is as follows:

 - (void)viewDidLoad {
[super viewDidLoad];

self.view.backgroundColor = [UIColor clearColor];
self.expensesTableView.backgroundColor = [UIColor clearColor];

self.theTableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
self.theTableView.rowHeight = 44;

UIView *containerView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 280, 44)] autorelease];
containerView.backgroundColor = [UIColor clearColor]; 

// Setting up and aligning the label in the center of our view.
UILabel *headerLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 20)];   
headerLabel.text = NSLocalizedString(self.headerText, @"");
headerLabel.textColor = [UIColor blackColor];
headerLabel.font = [UIFont boldSystemFontOfSize:16];
headerLabel.backgroundColor = [UIColor clearColor];
headerLabel.center = containerView.center;
headerLabel.textAlignment = UITextAlignmentCenter;

[containerView addSubview:headerLabel];

// HOW IS THIS A LEAK? CAUSES EXC_BAD_ACCESS If Not commented out
//NSLog(@"Test - %@", headerLabel);
//[headerLabel release];

[self.view addSubview:containerView];

NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
    // Update to handle the error appropriately.
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    exit(-1);  // Fail
}

}

The code above has the line headerLabel release] commented out. With this out the program runs fine! If I enable the line [headerLabel release], which I technically SHOULD have enabled since otherwise it's a LEAK, GDB then reports the following:

 MyAppTest[11110:207] *** -[CFString release]: message sent to deallocated instance 0x5d66b60
(gdb)

This is the output from malloc_history trace using the above memory address:

Identifier:      MyAppTest
Version:         ??? (???)
Code Type:       X86 (Native)
Parent Process:  gdb-i386-apple-darwin [10116]

Date/Time:       2011-03-15 03:14:47.855 -0400
OS Version:      Mac OS X 10.6.6 (10J567)
Report Version:  6

ALLOC 0x5996770-0x599678f [size=32]: thread_a037a540 |start | main | UIApplicationMain | -[UIApplication _run] | CFRunLoopRunInMode | CFRunLoopRunSpecific | __CFRunLoopRun | __CFRunLoopDoSource1 | __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ | PurpleEventCallback | _UIApplicationHandleEvent | -[UIApplication sendEvent:] | -[UIApplication handleEvent:withNewEvent:] | -[UIApplication _runWithURL:payload:launchOrientation:statusBarStyle:statusBarHidden:] | -[UIApplication _reportAppLaunchFinished] | CA::Transaction::commit() | CA::Context::commit_transaction(CA::Transaction*) | CALayerLayoutIfNeeded | -[CALayer layoutSublayers] | -[UILayoutContainerView layoutSubviews] | -[UINavigationController _startDeferredTransitionIfNeeded] | -[UINavigationController _startTransition:fromViewController:toViewController:] | -[AccountViewController viewWillAppear:] | -[NSManagedObjectContext executeFetchRequest:error:] | -[NSPersistentStoreCoordinator executeRequest:withContext:error:] | -[NSSQLCore executeRequest:withContext:error:] | -[NSSQLCore objectsForFetchRequest:inContext:] | -[NSSQLCore newRowsForFetchPlan:] | -[NSSQLCore _newRowsForFetchPlan:selectedBy:withArgument:] | -[NSSQLCore _prepareResultsFromResultSet:usingFetchPlan:withMatchingRows:] | CFStringCreateWithCString | __CFStringCreateImmutableFunnel3 | _CFRuntimeCreateInstance | malloc_zone_malloc 

Pulling my hair out because there is absolutely ZERO messages or calls made to headerLabel after I release it. Anyone have any suggestions? please??

---- UPDATE

- (void) viewWillAppear:(BOOL)animated {

[super viewWillAppear:animated];

[dataViewScroller.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];

[self.myTableViewControllers removeAllObjects];
self.myTableViewControllers = nil;

// All database modifcations should dynamically reload when the view reappears

/* RE-OBTAIN CATEGORIES */
NSManagedObjectContext *context = [myAppTestAppDelegate managedObjectContext];

NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"MyObjectType" inManagedObjectContext:context];
[fetchRequest setEntity:entity];

NSError *errorInRequest = nil;
NSArray *results = [context executeFetchRequest:fetchRequest error:&errorInRequest];

if(errorInRequest)
{
    NSLog(@"Unresolved error %@, %@", errorInRequest, [errorInRequest userInfo]);
    abort();
}

[fetchRequest release];

kNumberOfPages = [results count];

dataViewPageControl.numberOfPages = kNumberOfPages;
    dataViewPageControl.currentPage = 0;
dataViewScroller.contentSize = CGSizeMake(dataViewScroller.frame.size.width * kNumberOfPages, dataViewScroller.frame.size.height);

// the view controllers are created lazily
// in the meantime, load the array with placeholders which will be replaced on demand
NSMutableArray *controllers = [[NSMutableArray alloc] init];
for (unsigned i = 0; i < kNumberOfPages; i++) {
    [controllers addObject:[NSNull null]];
}

self.myTableViewControllers = controllers;
[controllers release];

int index = 0;

for (NSManagedObject *type in results) {
    [self loadScrollViewWithPage:index withIdentifier:[type valueForKey:@"name"]];
    index++;
}

}

- (void)loadScrollViewWithPage:(int)page withIdentifier:(NSString *)ident {

if (page < 0) return;
if (page >= kNumberOfPages) return;

MyTableListViewController *controller = [myTableViewControllers objectAtIndex:page];

if ((NSNull *)controller == [NSNull null]) {
    controller = [[MyTableListViewController alloc] initWithPageNumber:page withHeader:ident];
    controller.managedObjectContext = [myAppTestAppDelegate managedObjectContext];
    [myTableViewControllers replaceObjectAtIndex:page withObject:controller];
    [controller release];
}
if (nil == controller.view.superview) {
    CGRect frame = dataViewScroller.frame;
    frame.origin.x = frame.size.width * page;
    frame.origin.y = 0;
    controller.view.frame = frame;
    [dataViewScroller addSubview:controller.view];
}
}

So as you can see, they are always loaded dynamically when the Root ViewController loads. This is because the user can go to an Account Settings type admin page and add additional categories to their setup. When returning to the Root View the NEW / OR DELETED categories must be rendered in the scroll view. If they had 3 Cateogories (ie. 3 TableViewControllers) initially and went to the admin page and created a new one, when returning the the UIScrollView should reload with all 4 categories (4 dynamically created TableViewControllers.) Does this make sense?


Solution

  • Thanks to @e.James and @mvds for helping trace this down. Based off the malloc history in this post and @e.James initial suggestion to investigate the fetch I managed to drill down deeper and find the culprit issue.

    It was the following code which gets called to create and alloc on of the UIViewControllers found in the UIScrollView:

    // Load the view nib and initialize the pageNumber ivar.
    - (id)initWithPageNumber:(int)page withHeader:(NSString *)header withType:(NSManagedObject *)type {
    if (self = [super initWithNibName:@"ExpenseListViewController" bundle:nil]) {
    
        pageNumber = page;
                // below WAS the offending line.
        // headerText = header;
                self.headerText = header;
    
    }
    return self;
    

    }

    Now it it's correct and everything works as intended. Basically I was not using the setter method on a retained & synthesized property (headerText=header). Adding the self. fixed everything.