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..