1

Within an application, I'm wondering why an instance of a class's deinit method is not being called when quitting the application.

As an example, the Test class presented here is created in the AppDelegate's applicationDidFinishLaunching.

import Cocoa

class Test {
    let testVar = 1

    init() {

        print("Retain count \(CFGetRetainCount(self))")
        NSApplication.shared().terminate(self)
    }

    deinit {
        print("Calling deinit")
    }
}


@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    //@IBOutlet weak var window: NSWindow!    

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Insert code here to initialize your application

        _ = Test()  
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application            

        print("Terminating")
    }
}

Not only does this fail to call Test's deinit method, but the retain count in Test's init is 2; I would have expected this to be 1.

If an optional reference is stored in the AppDelegate class and set when creating the Test instance, it is nil when applicationWillTerminate is called

Can someone please explain why the retain count is 2 here and how to ensure that deinit of Test is called when the application is terminated?

TheDarkKnight
  • 27,181
  • 6
  • 55
  • 85
  • What do you mean by "optional" reference. Do you mean a Swift optional type or do you mean a weak reference? – allenh Jul 14 '17 at 20:47
  • @AllenHumphreys, sorry for not being clear, it's the same result with both an optional type and a weak reference. – TheDarkKnight Jul 15 '17 at 17:25

3 Answers3

2

I assume the Swift situation is the same as the Objective-C one documented at this link: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmRules.html#//apple_ref/doc/uid/20000994-BAJHFBGH

"When an application terminates, objects may not be sent a dealloc message. Because the process’s memory is automatically cleared on exit, it is more efficient simply to allow the operating system to clean up resources than to invoke all the memory management methods."

Phillip Mills
  • 30,888
  • 4
  • 42
  • 57
  • That makes sense, though annoying as it breaks the expectation of the language semantics; at least it's documented and thanks for pointing that out. – TheDarkKnight Jul 15 '17 at 17:29
1

I can't speak to why the retain count is 2. In general, with ARC enabled, you really shouldn't ever inspect the retain count, and the reason why is a well answered question

Additionally, there is an answer that suggests CFGetRetainCount might actually be increasing the retain count when you call it.

In your situation, deinit is not being called because you're terminating the app programmatically before the initializer is finished.

In the situation of having an instance of Test assigned to a variable on your AppDelegate, deinit is not called because AppDelegate is not released in the "normal" way when an app exits. Therefore, all of it's properties are not released. If you set the variable to nil in applicationWillTerminate, you'll see that deinit is then called appropriately. The for this behavior is explained when talking about global variables in this answer.

To provide a specific permutation of your provided example:

class Test {
    let testVar = 1

    init() {
        print("Retain count \(CFGetRetainCount(self))")
    }

    deinit {
        print("Calling deinit")
    }
}

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    var variable: Test?

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        variable = Test()

        DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
            // If you're trying to pass in `variable`, the terminate funciton will retain it
            NSApplication.shared.terminate(nil)
        }
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        variable = nil
    }
} 

deinit is only guaranteed to be called if the instance is release by ARC, but if release is never called, for instance, if the app crashes or is force quite by the user it won't be So, don't rely on it for absolutely critical "clean up".

Circling back on the retain count. Your code, executed exactly as is in your question, produces the following:

enter image description here

What we're seeing is that the retain count being incremented by one during the call to CFGetRetainCount, then decremented when it returns, then incremented again once more when it's passed to terminate.

allenh
  • 6,582
  • 2
  • 24
  • 40
  • Cmd+q produces the same result; deinit isn't being called and the program is terminating cleanly. – TheDarkKnight Jul 15 '17 at 17:28
  • Yeah, now that I made it a property on the app delegate I see that deinit is not being called – allenh Jul 15 '17 at 17:32
  • It's really frustrating, as it breaks the expectation of the language semantics, so I guess that means that we can't use it to clean-up a class, if we think the process can exit and must handle it some other way. – TheDarkKnight Jul 15 '17 at 17:34
  • What kind of resources are you trying to cleanup? I think `deinit` is only intended to cleanup memory resources used by a particular instance. Those resources don't needed cleaned up if the app exits. – allenh Jul 15 '17 at 17:36
  • The application sets various fields in an SQLite3 database when running and I'm wanting to change them back on exit. – TheDarkKnight Jul 15 '17 at 17:39
  • `applicationWillTerminate` for a macOS or iOS application is the way to go, but even that isn't guaranteed. My first instance is to have a safe shutdown mechanism such as this, and then detecting unsafe shutdown upon relaunch and trying to recover. But maybe there are more actors than I'm aware of. – allenh Jul 15 '17 at 18:19
  • Thanks for the edit; that certainly clarifies the retain count. **If you set the variable to nil in applicationWillTerminate, you'll see that deinit is then called appropriately.** - I must be doing something different, as this still doesn't call `deinit` for me, which is why my example demonstrates without storing it as a variable. – TheDarkKnight Jul 15 '17 at 18:36
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/149297/discussion-between-thedarkknight-and-allen-humphreys). – TheDarkKnight Jul 15 '17 at 18:50
1

The issue is due to terminating the application from within the init of the Test class. I suspect that calling terminate in the init is preventing the correct instantiation of the class, so its deinit is never called.

By delaying the call to terminate, the call to Test's deinit is called, as expected

import Cocoa

class Test {

    init() {

        DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
            NSApplication.shared().terminate(self)
        }
    }

    deinit {
        print ("Calling Deinit")
    }
}


@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    //@IBOutlet weak var window: NSWindow!
    var variable: Test?

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Insert code here to initialize your application

        variable = Test()

    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application

        variable = nil
        print("Terminating")
    }
}
TheDarkKnight
  • 27,181
  • 6
  • 55
  • 85