Retain cycle when NSWindow is its own delegate

I was noticing that my NSWindowControllers were never being deallocated when windows were closed, and I narrowed it down to an issue where an NSWindow seems to have a strong reference to its delegate, despite the



class SimpleWindow : NSWindow, NSWindowDelegate {

/// Debug variable to see how many instances are around

public private(set) static var InstanceCount = 0


deinit {

SimpleWindow.InstanceCount -= 1

}


override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) {

super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)

SimpleWindow.InstanceCount += 1

}

}


class WindowReferenceTests: XCTestCase {

func testWindows() {

let startCount = SimpleWindow.InstanceCount

do {

let wc = SimpleWindow()

wc.delegate = wc // weak open var delegate: NSWindowDelegate?

XCTAssertEqual(startCount + 1, SimpleWindow.InstanceCount)

}

XCTAssertEqual(startCount, SimpleWindow.InstanceCount, "window controllers have circular references somewhere")

}

}

Sorry, I accidentally posted the incomplete question above. Anyway, to complete the question: I was noticing that my NSWindowControllers were never being deallocated when windows were closed, and I narrowed it down to an issue where an NSWindow seems to have a strong reference to its delegate, despite the fact that the delegate var is marked as weak. I can reproduce the issue in the test case (in the above post), where when I set an NSWindow as its own delegate, it is never de-inited.

It's not clear that the test you posted is actually telling you that the window was not deallocated. Instead, it's telling you that the InstanceCount class variable is wrong.


On the face of it, that's because you create the window like this:


            let wc = SimpleWindow()


Your SimpleWindow subclass doesn't have an initializer with no parameters. You declare an override to the designated NSWindow initializer, and — since that's the only designated initializer — your subclass inherits the two convenience initializers.


AFAICT, the parameter-less initializer shouldn't be available at all (for creating instances of SimpleWindow). I think it's a defect of the Obj-C initializer mechanism, which doesn't have the same strict rules as the Swift initializer mechanism. So, in your code, SimpleWindow() doesn't look like it will increment InstanceCount at all.


If this is all just an artifact of your reduced code fragment, and you really do have a retain cycle, it's likely not because of some misbehavior in the weak delegate variable. Instead, it probably indicates that you have some other circular reference resulting from using the window as its own delegate.

Sorry, I should have clarified: when I remove the line "wc.delegate = wc", then the window instance count is successfully decremented, indicating that something having to do with the delegate assignment is causing a strong reference cycle to happen.


SimpleWindow.InstanceCount is indeed being incremented when the window is created, and it is decremented when the window falls out of scope unless the delegate has been assigned.

The chances that there is a bug where a property declared weak open var delegate: NSWindowDelegate? will contain a strong reference are effectively zero. It'd be too big a bug to show up only in this restricted context.


It's more likely that the presence of the delegate means that one of its implemented delegate methods does something that creates a reference cycle as a side effect, such as storing a closure that captures self strongly.


This does seem like something you can investigate with the Allocations tool in Instruments.

And yet the delegate is not being released. None of the delegate methods are implemented.


Poking a bit further, it appears that if I put the delegate in a separate class, the NSWindow instance is released, but the NSWindowDelegate is never released, even if I nil out the delegate reference prior to the NSWindow being released! Try this simple test case yourself on 10.13 and you'll see:


import XCTest
import AppKit

class SimpleWindow : NSWindow {
    /// Debug variable to see how many instances are around
    public private(set) static var InstanceCount = 0

    deinit {
        SimpleWindow.InstanceCount -= 1
    }

    override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) {
        super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)
        SimpleWindow.InstanceCount += 1
    }
}

class SimpleWindowDelegate : NSObject, NSWindowDelegate {
    /// Debug variable to see how many instances are around
    public private(set) static var InstanceCount = 0

    deinit {
        SimpleWindowDelegate.InstanceCount -= 1
    }

    override init() {
        super.init()
        SimpleWindowDelegate.InstanceCount += 1
    }
}

class WindowDelegateTests: XCTestCase {

    func testWindows() {
        XCTAssertEqual(0, SimpleWindow.InstanceCount)
        XCTAssertEqual(0, SimpleWindowDelegate.InstanceCount)
        do {
            let wc = SimpleWindow()
            let wdc = SimpleWindowDelegate()
            wc.delegate = wdc
            XCTAssertEqual(1, SimpleWindow.InstanceCount)
            XCTAssertEqual(1, SimpleWindowDelegate.InstanceCount)
            wc.delegate = nil // even manually nilling the delegate doesn't release it
        }
        XCTAssertEqual(0, SimpleWindow.InstanceCount, "window was not released")
        XCTAssertEqual(0, SimpleWindowDelegate.InstanceCount, "window delegate was not released") // XCTAssertEqual failed: ("0") is not equal to ("1") - window delegate was not released
    }

}

I dunno. Perhaps it's something to do with the responder chain. (The documentation says that the delegate is added to the responder chain.)


There are still a couple of weird things in your code:


1. You're still creating the window with an invalid init. You should use your subclass's designated init.


2. The lifetime of the SimpleWindowDelegate object should end (theoretically) after line 40 of your last sample code. At that point, there should be no strong references to it (neither the delegate property nor the responder chain pointer are owning references).


3. Can you reproduce this behavior outside of a test case? It's not clear what test code generation might do.

Retain cycle when NSWindow is its own delegate
 
 
Q