2

I'm trying to figure out why an NSWindow instance is never getting freed in a project I'm working on.

I've managed to get the minimal repro case down to the following single file project that you can execute from the command line. As far as I can tell I'm not doing anything wrong here, but I'd love to be proven wrong!

$ clang main.m -fobjc-arc -xobjective-c++ -framework Cocoa && ./a.out
#import <Cocoa/Cocoa.h>

@interface Delegate: NSObject<NSWindowDelegate>
@end

@interface MyWindow: NSWindow
@end

int main() {
    @autoreleasepool {
        // Initialize the shared application
        NSApplication* app = NSApplication.sharedApplication;

        // Allow UI
        app.activationPolicy = NSApplicationActivationPolicyRegular;

        // Create the window and delegate
        NSWindow *window = [[MyWindow alloc] init];
        Delegate *delegate = [[Delegate alloc] init];
        window.delegate = delegate;

        // Have the window maintain normal refcount semantics
        window.releasedWhenClosed = NO;

        // Finish launching the app
        [app finishLaunching];
        [NSRunningApplication.currentApplication
            activateWithOptions:NSApplicationActivateAllWindows];

        // Show the window
        [window orderFront:nil];

        // Simulate the user clicking the x (canceled by the delegate). If we
        // comment out this line, or don't cancel the close in the delegate, we
        // don't get the leak.
        [window performClose:nil];

        // Actually close the window
        [window close];
    }

    NSLog(@"exit process");
}

@implementation Delegate
    - (instancetype)init {
        NSLog(@"[delegate init]");
        return [super init];
    }

    - (BOOL)windowShouldClose:(NSWindow *)sender {
        NSLog(@"[delegate windowShouldClose:] (returning NO to cancel)");
        return NO;
    }

    - (void)windowWillClose:(NSNotification *)notification {
        NSLog(@"[delegate windowWillClose:]");
    }

    - (void)dealloc {
        NSLog(@"[delegate dealloc]");
    }
@end

@implementation MyWindow
    - (instancetype)init {
        NSLog(@"[MyWindow init]");
        return [super
            initWithContentRect:NSScreen.mainScreen.visibleFrame
            styleMask:NSWindowStyleMaskTitled |
                      NSWindowStyleMaskClosable |
                      NSWindowStyleMaskMiniaturizable |
                      NSWindowStyleMaskResizable
            backing:NSBackingStoreBuffered
            defer:NO
        ];
    }

    - (void)dealloc {
        NSLog(@"[MyWindow dealloc]");
    }
@end

When I run the above code, I see this output:

$ clang main.m -fobjc-arc -xobjective-c++ -framework Cocoa && ./a.out
[MyWindow init]
[delegate init]
[delegate windowShouldClose:] (returning NO to cancel)
[delegate windowWillClose:]
[delegate dealloc]
exit process

I expected [MyWindow dealloc] to be called somewhere in there, but it was not. If I comment out the call to performClose: or return YES instead of NO from windowShouldClose:, then it does in fact deallocate as expected.

NOTE: You may notice that I'm not giving Cocoa a chance to execute any events. My real project of course does, if you're concerned that there's some event queued up that's retaining the window, I tried to rule that out using this (and variations of this with different run loop modes):

for (;;) {
    NSEvent *event = [app nextEventMatchingMask:NSEventMaskAny
        untilDate:nil
        inMode:NSDefaultRunLoopMode
        dequeue:YES];

    if (event) {
        [app sendEvent:event];
    } else {
        break;
    }
}

[EDIT] As discovered in the comments and by having friends test it, on some machines the window is consistently freed as expected, on some it never is. As suggested in the comments, I generated a graph indicating who still retains the window in Xcode on a machine I experience the problem on: an annotated text version of that graph can be found here..

Mason
  • 33
  • 1
  • 5
  • 2
    I am running your code via xcode and its showing [MyWindow dealloc] in log. Xcode 10, macOS 10.14.6 – Parag Bafna Aug 22 '19 at 10:01
  • Same here. I have Xcode 10.3 (10G8) and macOS 10.14.6 and it shows `[MyWindow dealloc]` just before `exit process`. I even used your compilation command in the shell. Is your system configuration different? What does `clang --version` say? – Michael Aug 22 '19 at 16:16
  • Thanks for trying it--that's really odd. I'm on macOS 10.14.15, Apple LLVM version 10.0.1 (clang-1001.0.46.4). This is gonna sound weird but--do you either of you have a Touch Bar? It's hard to tell because there's so much activity, but in Instruments it looks like something the system is doing automatically w/ the Touch Bar might be retaining and not subsequently releasing the window, but I'm not 100% certain. – Mason Aug 22 '19 at 17:51
  • No `[MyWindow dealloc]` with the Xcode touchbar on macOS 10.13. Does it matter if the app quits? – Willeke Aug 22 '19 at 19:49
  • Interesting, so it's not just my machine. It should print `exit process` and then subsequently quit--is that what's happening for you? – Mason Aug 22 '19 at 21:28
  • @Michael @Parag I went ahead and updated to macOS 10.14.6 and `Apple LLVM version 10.0.1 (clang-1001.0.46.4)` and I still experience the problem, so the only lead I have left atm would be if you both have macs that don't have a Touch Bar. – Mason Aug 23 '19 at 03:34
  • @Mason: I have no touch bar. Your theory seems still plausible. – Michael Aug 23 '19 at 10:12
  • The way to answer this question is to set a breakpoint after you think the window should have been destroyed then use Xcode's memory graph tool or the allocation instrument to find out who still retains the window. – James Bucanek Aug 23 '19 at 16:40
  • @James thanks for the tip, I didn't know about this tool! I went ahead and generated the graph, unfortunately none of the things retaining the window seem to originate from my code. [Here's an annotated text version of the graph](https://gist.github.com/MasonRemaley/4d9983c6a51efe9dd8cfc436db6619de) for anyone who wants to take a look. – Mason Aug 23 '19 at 19:54
  • Ah, I just spotted this in your code: `window.releasedWhenClosed = NO;` Think about that a second ;) This property defaults to `YES`, and probably explains what you're seeing. – James Bucanek Aug 23 '19 at 21:20
  • 1
    It is difficult to tell from this graph what the source of the retains are. I'll often switch to using the allocation instrument, that way you can review every retain to your window and the stack trace it came from. I suspect it's something that has retained/autoreleased your window and it will/would get resolved on the next event loop. – James Bucanek Aug 23 '19 at 22:08
  • @James Unfortunately that's intentional, `releasedWhenClosed` isn't compatible w/ using ARC + programmatically created windows, enabling it actually results in over-releasing the window. It doesn't appear to be the event queue either--at the bottom of my question I have a snippet I tried using to flush the event queue, assuming I'm using that API correctly flushing it doesn't make a difference. I'll check out the allocation instrument again, but I think at this point sadly I'm operating under the assumption that this is a macOS bug that I'm probably not going to be able to solve. :\ – Mason Aug 28 '19 at 00:44
  • When I find the time to switch back over to the macOS platform layer for this project I'll also go ahead and and make a variant of this example that /doesn't/ exit the process (or the event loop) after the window is closed, to rule out the possibility that the Window eventually gets freed after some time has passed. Who knows maybe that'll resolve it. – Mason Aug 28 '19 at 00:52

0 Answers0