5

I have window controller and view controller that use core data bindings, but I want to be able to have those views really, truly get deallocated. I then want to reset the managedObjectContext and at that point have given up as much memory as possible.

I have found that I am required to unbind things which I've bound in the Nib, or the MOC keeps the Nib objects retained.

The issue is this EXEC_BAD_ACCESS trace:

enter image description here

If I do not unbind all of the bindings, even those created in Interface Builder, reset on the MOC causes an EXEC_BAD_ACCESS because bindings are attempting to reflect changes in the MOC in the view that's gone, through an array controller that should be gone, but isn't.

So I did this in the window controller's dealloc:

- (void) dealloc 
{
    NSLog(@"Wincon dealloc");
    @autoreleasepool {
        // Remove subview to ensure subview dealloc
        [_viewController.view removeFromSuperviewWithoutNeedingDisplay];

        // Tear down bindings to ensure MOC can reset
        for (NSObject<NSKeyValueBindingCreation>* object in
             @[_watcherAC, _watchersTimesTreeController, _watcherTableView, _itemsOutlineView])
        {
            for (NSString* binding in [object exposedBindings])
                [object unbind:binding];
        }
    }
}

which is triggered this way:

- (void) switchToBackgroundMode
{
    NSLog(@"SwitchToBackgroundMode");

    // Hide the menu and dock icon
    [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];

    // Force every view to deallocate before reset
    @autoreleasepool {
        // Need to check loaded to prevent closing a closed window and
        //  triggering a second call to applicationShouldTerminateAfterLastWindowClosed
        if ([self.wincon isWindowLoaded]) [self.wincon close];
        self.wincon = nil;
    }

    NSLog(@"About to resetCoreDataStack");
    [self resetCoreDataStack];
}

... and now I don't get any errors with that resetCoreDataStack

The stack trace above comes with a log file like this:

2014-05-29 15:54:35.794 MyApp[10230:303] Switch to BG in appShouldTerminate
2014-05-29 15:54:35.794 MyApp[10230:303] SwitchToBackgroundMode
2014-05-29 15:54:35.808 MyApp[10230:303] Wincon dealloc
2014-05-29 15:54:35.830 MyApp[10230:303] About to resetCoreDataStack
2014-05-29 15:54:35.830 MyApp[10230:303] Reset Core Data
{Exception thrown iff wincon dealloc doesn't unbind everything}

And so the window controller dealloc is definitely called when it's nilled in the autoreleasepool, but MOC reset causes an EXEC_BAD_ACCESS unless that wincon dealloc does unbind on a bunch of crap in the Nib.

So the question is:

Given a Nib owned by a custom window controller (self.wincon) with arrayController objects bound to an external managedObjectContext, what needs to be done to force everything in the Nib to be released and unbound? Is there some step that I'm missing that causes me to have to do this unbinding manually?


[EDIT] Some new debug code:

NSLog(@"Wincon dealloc");

@autoreleasepool {
    // Remove subview to ensure subview dealloc
    [_viewController.view removeFromSuperviewWithoutNeedingDisplay];
    _viewController = nil;

    self.window = nil;
}

@autoreleasepool {

    // Tear down bindings to ensure MOC can reset
    for (NSObject<NSKeyValueBindingCreation>* object in
         @[_watcherAC, _watchersTimesTreeController, _watcherTableView, /*_itemsOutlineView*/])
    {
        NSLog(@"Bindings for %@", [object className]);
        for (NSString* binding in [object exposedBindings]) {
            NSLog(@"BI for %@: %@", binding, [object infoForBinding:binding]);
            [object unbind:binding];
        }
    }

the log below is the bindingInfo for the bindings still alive when dealloc is called for the windowController

2014-05-29 21:00:39.967 SaleWatch[11249:303] Wincon dealloc

2014-05-29 21:00:39.975 SaleWatch[11249:303] Bindings for NSArrayController

2014-05-29 21:00:39.978 SaleWatch[11249:303] Bindings for NSTreeController
2014-05-29 21:00:39.989 SaleWatch[11249:303] BI for contentSet: {
    NSObservedKeyPath = "selection.fetchTimesForOutlineView";
    NSObservedObject = "[entity: SWWebStoreWatcher, number of selected objects: 1]";
}

2014-05-29 21:00:39.991 SaleWatch[11249:303] Bindings for NSTableView
2014-05-29 21:00:39.991 SaleWatch[11249:303] BI for selectionIndexes: {
    NSObservedKeyPath = selectionIndexes;
    NSObservedObject = "[entity: SWWebStoreWatcher, number of selected objects: 1]";
}
2014-05-29 21:00:40.001 SaleWatch[11249:303] BI for content: {
    NSObservedKeyPath = arrangedObjects;
    NSObservedObject = "[entity: SWWebStoreWatcher, number of selected objects: 1]";
}

2014-05-29 21:00:40.020 SaleWatch[11249:303] About to resetCoreDataStack

I forced wincon.window = nil in the new code, and these three objects still aren't nil, though the outlineView the treeController is for did become nil. There could be a retain cycle here, but I don't see how it'd be my fault... yet.

stevesliva
  • 5,351
  • 1
  • 16
  • 39
  • Is the window controller the owner of the NIB? Is the array controller instantiated in the NIB? If the window is closed and its owning window controller has deallocated, what has kept the table view retained? (I don't think it's the binding from the table view to the MOC.) Use the Allocations instrument to track that down. – Ken Thomases May 30 '14 at 00:18
  • @KenThomases loadNib is what's showing up in the stack for the still-retained tableView in allocations. And yes, the the windowController was created with initWithNibName for the suspect Nib here. That init is what's in the retain stack for the tableView after the windowController gets sent dealloc. – stevesliva May 30 '14 at 03:10
  • What bindings of the array controller are bound and to what? Similarly, what bindings of the table view are bound and to what? Is anything bound *to* the table view? (It would be odd to bind to a view object, but I'm thinking there may be an odd setup that's causing a retain cycle.) – Ken Thomases May 30 '14 at 03:19
  • That's a good point about what is the AC bound to. Bound to "Application" with a keypath of "delegate.managedobjectcontext" -- I think I should try to make the MOC a weak property of the windowController and see if this issue goes away. (Most everything else is, I think, fairly conventional) – stevesliva May 30 '14 at 03:24
  • @KenThomases - that wasn't it, though I meant to do that anyways. The other AC binding is sortDescriptors is bound to a strong NSArray property of the windowController. – stevesliva May 30 '14 at 03:44
  • That binding sounds reasonable to me. I was suspecting something was binding back to the table view. In your loop where you unbind, you might log the binding info (`-infoForBinding:`). – Ken Thomases May 30 '14 at 03:47
  • @KenThomases new information added. The AC bindings are all gone by dealloc. But the tableView and treeController bindings to the AC are still alive. Does that somehow create a cycle ***I*** need to break? I don't see it. It is weird that the tableView persists when the outlineView doesn't, though. – stevesliva May 30 '14 at 04:27
  • Assuming you do the unbind trick to avoid the crash, does the table view leak? – Ken Thomases May 30 '14 at 06:59
  • With unbind, leaks doesn't mark a leak and I can't find any tableView object left. w/o unbind, leaks crashes at the EXEC_BAD_ACCESS. I'm not sure whether to call this a retain cycle, or an NSAutoUnbinder bug. – stevesliva May 30 '14 at 16:26

0 Answers0