Working without a nib, Part 12: NSWindow memory management

April 21 2020 by Jeff Johnson

I've been working from home for a long time. Now that many of you have joined me in working without an office, will you also join me in working without a nib? You might ask why, and as luck would have it, my previous blog post in the "Working without a nib" series explained why! After why, the next logical question is how. In this blog post I'll talk about a subject that's always been particularly tricky in Cocoa: NSWindow memory management. When you work with a nib, NSWindowController "magically" handles NSWindow memory management. Of course, that just pushes the problem up to the meta-level, because nothing magically handles NSWindowController memory management. Anyway, when you work without a nib, you don't need NSWindowController. So how does NSWindow memory management work?

As a reminder, I explained earlier in this series (in 2016, gulp!) that even under Automatic Reference Counting (ARC), which doesn't use retain and release, NSWindow requires you to unset releasedWhenClosed, otherwise there's an overrelease crash when the window closes. Still, the question remains, how exactly does NSWindow memory management work under ARC? It turns out that this changed in macOS 10.13 High Sierra! The change is documented (in some sense of documented) in the (archived) AppKit Release Notes for macOS 10.13:

NSWindow Lifecycle Changes (Updated since Seed 2)

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

So, consider the following code:

NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(300.0, 300.0, 300.0, 300.0) styleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskClosable) backing:NSBackingStoreBuffered defer:YES];
[window setReleasedWhenClosed:NO];
[window makeKeyAndOrderFront:nil];

If your app uses the 10.13 SDK or later, the above code will display a window on the screen and deallocate the window when you close it. On the other hand, if your app uses the 10.12 SDK or earlier, the above code will deallocate the window. Notice that I didn't say it will display a window on the screen? Since the local variable window is the only reference to the NSWindow, the NSWindow will deallocate after the last reference in the code. The app won't crash, but you won't see a window. Don't blink!

Let's see why this happens. We can use otool -tV /System/Library/Frameworks/AppKit.framework/AppKit (or my own riptool) to see the AppKit disassembly:

000000000017a606	pushq	%rbp
000000000017a607	movq	%rsp, %rbp
000000000017a60a	movl	$0xd, %edi
000000000017a60f	callq	0xb7a988 ## symbol stub for: __CFExecutableLinkedOnOrAfter
000000000017a614	movsbl	%al, %eax
000000000017a617	popq	%rbp
000000000017a618	retq

The function NSApplicationStronglyReferencesOpenWindowsDefaultValueFunction returns the value from the function CFExecutableLinkedOnOrAfter called with the argument 0xd, which is hexadecimal for the decimal number 13. Which means the macOS 10.13 SDK. The function NSApplicationStronglyReferencesOpenWindowsDefaultValueFunction is called by the private methods -[NSApplication _addOpenWindow:] and -[NSApplication _removeOpenWindow:]. So what happens is that NSApplication keeps a strong reference to all open windows in its ivar _openWindows if the app uses the 10.13 SDK or later, otherwise it doesn't keep a strong reference, and you're on your own. Note that the 10.13+ behavior can be overridden by setting the NSUserDefault NSApplicationStronglyReferencesOpenWindows to false, which gives you the 10.12- behavior again.

I don't have a macOS 10.12 Sierra volume to test with anymore, unfortunately, but we can be reasonably sure that this CFExecutableLinkedOnOrAfter check does not exist in the AppKit version on 10.12. Thus, you should not rely on the newer 10.13 SDK memory management behavior if your app has a deployment target of 10.12 or lower. Beware! The good news, however, is that if your app does have a deployment target of 10.13 or higher, then the memory management of NSWindow becomes relatively simple. It's almost "magical" again, and we didn't need NSWindowController.

Jeff Johnson (My apps, PayPal.Me)