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