Search code examples
swiftmacosnswindownswindowcontroller

Programmatically created NSWindow crashes upon close


I am trying to wrap my head around proper lifecycles of NSWindows and how to handle them properly.

I have the default macOS app template with a ViewController inside a window that is inside a Window Controller.

I also create a simple programatic window in applicationDidFinishLaunching like this:

let dummyWindow = CustomWindow(contentRect: .init(origin: .zero, size: .init(width: 200, height: 100)), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: true)
dummyWindow.title = "Code window"
dummyWindow.makeKeyAndOrderFront(nil)

The CustomWindow class is just:

class CustomWindow: NSWindow {
    deinit {
        print("Deinitializing window...")
    }
}

When I close the programatic window (either by calling .close() or by just tapping the red close button, the app crashes with EXC_BAD_ACCESS. Even though I am not accessing the window in any way.

One might think it's because of ARC but it's not. One—the window is still strongly referenced by NSApplication.shared.windows even when the local scope of applicationDidFinishLaunching ends. And two—the "Deinitializing window..." is only printed after the window is closed.

Closing the Interface Builder window works without any crashes. I dug deep and played with the isReleasedWhenClosed property. It made no difference whether it was false or true for the IB window. It stopped the crashing for the programmatic window though.

But this raises three questions:

  1. What is accessing the programatic window after it's closed—causing a crash because the default behaviour of NSWindow is to release it—if it's not my code?
  2. What is the difference under the hood between a normal window and a window inside a window controller that prevents these crashes?
  3. If the recommended approach for programmatic windows is to always set isReleasedWhenClosed = false then how do you actually release a programatic window so that it does not linger in memory indefinetely?

Solution

  • 1. What is accessing the programatic window after it's closed—causing a crash because the default behaviour of NSWindow is to release it—if it's not my code?

    The backtrace of the crash shows that the window is referenced from the autorelease pool. The window has already been deallocated.

    2. What is the difference under the hood between a normal window and a window inside a window controller that prevents these crashes?

    From the documentation of isReleasedWhenClosed:

    Release when closed, however, is ignored for windows owned by window controllers.

    This means that a window owned by a window controller doesn't look at isReleasedWhenClosed and the window is not released when closed. The window controller owns and releases the window.

    3. If the recommended approach for programmatic windows is to always set isReleasedWhenClosed = false then how do you actually release a programatic window so that it does not linger in memory indefinetely?

    The recommended approach for programmatic windows is to set isReleasedWhenClosed to false so the window follows the ARC rules. If there are no strong references then the window will be deallocated. You can use NSWindowDelegate method windowWillClose(_:) or willCloseNotification to release the window in case you keep a strong reference.

    AppKit retains windows. I think this is for backwards compatibility. This explains why the window doesn't disappear when dummyWindow goes out of scope and the window is released.

    If your application is linked on macOS 10.13 SDK or later, NSWindows that are ordered-in will be strongly referenced by AppKit, until they are explicitly ordered-out or closed (not including hide or minimize operations). This means that, generally, an on-screen window will not be deallocated (and close/order-out as a side-effect of deallocation).

    Source: AppKit Release Notes for macOS 10.13

    A bit of history:

    1. Objective-C and manual retain-release (MRR, before ARC)

    When ARC didn't exist and we had to manually retain and release our objects, it was possible to create an instance and let it float in memory. There were no references to this object. This 'feature' was used for delegates and controllers that release themself or would exist untill the app quit. It also worked for windows. But then the window would leak when the user closes the window. Solution: set releasedWhenClosed to YES (default) and the window is automagically released when it is closed.

    Example 1, window is not reused

    - (void)showMyWindow {
        NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(100, 100, 500, 500)
            styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable
            backing:NSBackingStoreBuffered defer:NO];   // retain count = 1
        [window makeKeyAndOrderFront:self];
    }
    

    When the user closes the window, the window releases itself (releasedWhenClosed = true) (retain count = 0) and is deallocated.

    Example 2, window is reused

    ivar: NSWindow *myWindow

    - (void)showMyWindow {
        if (!myWindow) {
            myWindow = [[NSWindow alloc] initWithContentRect:NSMakeRect(100, 100, 500, 500)
                styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable
                backing:NSBackingStoreBuffered defer:NO];   // retain count = 1
            [myWindow setReleasedWhenClosed:NO];
        }
        [myWindow makeKeyAndOrderFront:self];
    }
    

    When the user closes the window, the window does not release itself (retain count = 1) and is not deallocated.

    2. And then came ARC:

    Example 1 with ARC:

    - (void)showMyWindow {
        NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(100, 100, 500, 500)
            styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable
            backing:NSBackingStoreBuffered defer:NO];   // retain count = 1
        [window makeKeyAndOrderFront:self];
        // window goes out of scope and is released (retain count = 0)
    }
    

    The window is deallocated immediately and disappears from the screen. A disappearing window is a problem so from OS X 10.13 the window is retained by AppKit while it is on screen.

    - (void)showMyWindow {  // 10.13+
        NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(100, 100, 500, 500)
            styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable
            backing:NSBackingStoreBuffered defer:NO];   // retain count = 1
        [window makeKeyAndOrderFront:self]; // retained by AppKit, retain count = 2
        // window goes out of scope and is released, retain count = 1
    }
    

    When the user closes the window, AppKit releases the window (retain count = 0) and the window releases itself (releasedWhenClosed = true) (retain count = -1). The app crashes.

    With ARC it is not possible to retain and not release a window so isReleasedWhenClosed should be false.

    Note: The retain counts are not the value of retainCount, the window is retained and (auto)released many times by AppKit.