Reputation: 133189
In Objective-C with ARC or Swift (which always uses ARC), what difference does it make whether isReleasedWhenClosed
is true
or not? Won't a window be kept alive as long as my code has strong references to it, and automatically destroyed when the last strong reference is gone? So why is this setting needed at all?
Upvotes: 0
Views: 158
Reputation: 133189
tl;dr? Scroll down all the way to the final summary. Yet if you want to know the history of that setting, how it really works and why it is even there, see below.
The isReleasedWhenClosed
property has been a major source of confusion for decades. Even in modern Swift code, people misunderstand this property and its purpose, or how it may or may not affect their code and application behavior. To understand why isReleasedWhenClosed
even exists, we have to go back to a time when Swift didn't exist and Objective-C had no automatic memory management or garbage collection, it had no weak references and it didn't even have synthesized properties (@propertry
), you had to write getter/setter methods yourself.
Before ARC, you had to manage memory yourself in Obj-C. That is, if you created an object using a method starting with new...
or alloc...
, you had to release that object when you were done with it by calling release
on it. Now, if you passed that object around and someone else wanted to keep a persistent reference to it, they had to retain
the object, which would prevent it from being destroyed when its creator called release
. But if you retained an object, you also had to offset that retention with another release
, otherwise the object would never be destroyed.
What happens if you keep a reference to an object without holding it? In this case, the object may be destroyed and your reference becomes a dangling pointer. A dangling pointer is a pointer to an address where an object used to be, but that object no longer exists, so there is either nothing or another object at that address, or the address points to the middle of some memory allocation. There's nothing wrong with having dangling object pointers, they're perfectly legal as long as you don't actually try to send them messages, because that will result in undefined behavior, meaning maybe it will cause your application to crash, maybe it will cause your application to behave in weird ways, just don't do it.
So the rules for proper memory management were basically:
In modern Obj-C, this code is safe
@implementation SomeObject
{
id _ivar;
}
- (void)lookAtMyCoolObject:(id)object
{
_ivar = object;
}
@end
as object
gets retained the moment you assign it to _ivar
but in old school Obj-C that would be fatal. Instead you would have to write
_ivar = [object retain];
and to prevent the object from leaking, you had to add
- (void)dealloc
{
[_ivar release];
[super dealloc];
}
You can get the same behavior in modern Obj-C by declaring the variable as __unsafe_unretain
. For synthesized properties you would use assign
and in Swift you would use unowned
. In general, however, there is no need for this construct anymore, as weak references now exist and you would just declare variables as __weak
or use weak
for properties and in Swift. The advantage of weak references is that they never become dangling pointers. When the object they reference is released, they become nil
instead, and sending messages to nil
is defined behavior.
Now suppose you had a NIB file describing an NSWindow containing just a text label, named "MyWindow.nib"
, and you wanted to load that window into your application, how would you have done that in old school Obj-C code?
First, you would have created a class to manage that window. You could subclass NSWindowController
, but you didn't have to, you could just as well do this
@interface MyWindowController : NSObject
// ...
@end
@implementation MyWindowController
{
IBOutlet NSWindow * _window;
IBOutlet NSTextField * _label;
}
- (instancetype)init
{
self = [super init];
if (self) {
[[NSBundle mainBundle]
loadNibNamed:@"MyWindow" owner:self topLevelObjects:nil
];
}
return self;
}
@end
In the NIB file, set the MyWindowController
class as the owner of the NIB file, and then use drag'n drop in the Interface Builder to assign the window to _window
and the label to _label
. Done.
But wait, how does memory management work here? There are no retain
/release
calls anywhere in the code, and both instance variables do not retain anything just because a value is assigned to them. There is also no dealloc
that would release anything.
To make memory management for the UI easier, Apple decided that windows retain themselves on load, and a window retains everything found within the window. So MyWindowController
in fact retains nothing in the above code. It has an unsafe reference to the window, which stays alive because it has self-retained, and another unsafe reference to the label inside the window, which stays alive because the window has retained it.
What about freeing these objects? The code above is fine if the window exists as long as the application is running, because then there is no need to ever free anything, because when the application quits, all its resources are implicitly freed by the system anyway. However, sometimes an application has multiple windows that come and go, and then you also want to free the memory used by these UI objects at some point. And now we finally come to isReleasedWhenClosed
!
If set to true
, the moment the window is closed, it will release itself once to balance the self-retention on load. This causes the window to be destroyed, which causes the label to be released and destroyed as well. As a result, both instance variables above are now dangling pointers and cannot be used anymore. So typically you would want to make sure that when the window is closed, MyWindowController
is also destroyed, or at least no longer performs any operation that would access any UI objects. The typical way to do this is to tell the object that created and holds MyWindowController
alive when the window has closed, and that object would then release MyWindowController
and set its reference to nil
or notify someone else and get destroy itself.
However, if you set isReleasedWhenClosed
to false, then closing the window will not cause the window to receive a release
message and thus not balance the self-retaining. In this case, the window would leak unless you add this code to the controller
- (void)dealloc
{
[_window release];
[super dealloc];
}
This was a bit odd because it violated the rules I just stated above. You did not create the window (you loaded the NIB file, but never created a window object yourself), and you did not retain it, so according to the rules above, you should never have to release it. Therefore, the default behavior was that the window would retain and release itself, and you didn't have to worry about memory management at all.
Note that you only need to release the window itself, the label will be freed as a direct result of this. Releasing both will most likely crash or cause strange behavior. Why would you want this behavior? There are two situations where you might want this behavior:
You may want to access UI objects after the window has closed, e.g. you may not want to read values from input fields immediately after the input field has been changed or after the user has clicked a button, but only after the window has closed. This is only possible if these UI objects still exist when the window is closed.
You may want to bring the window back later without having to create a new controller and reload the window from the NIB file. All you need to do in the controller above is call [_window makeKeyAndOrderFront:self]
and the window is back on the screen in exactly the state it was in when it was closed.
Now that you understand what this setting was originally good for and how it interacted with classical memory management, you might also understand why this setting is pretty much useless in modern Obj-C or Swift most of the time. With automatic reference counting in Obj-C and Swift, the window is automatically retained by your custom controller the moment it is assigned to an instance variable or property; and so is any other UI object within the window that you reference directly. Unless you manually set the references to nil
or your controller object itself is destroyed, no window is ever destroyed when you set isReleasedWhenClosed
to true
and the window is closed.
In fact, the self-retain itself would be useless in modern code, with one exception: A weak reference to a window. If your window reference is weak (__weak NSWindow * _window
) and the window would not self-retain, it would be destroyed immediately after creation and the variable would immediately become nil
again. So there is still self-retaining, and if isReleasedWhenClosed
is true
, a weak variable will become nil
the moment the window is closed (assuming no one else has retained it), otherwise it will not and still refer to the window.
Will a window leak in modern Obj-C and Swift if isReleasedWhenClosed
is false
? After all, you cannot explicitly release the window in ARC and in Swift, so how would you compensate for the self-retaining?
The answer is: Not if the owner is also destroyed. If you go back to the NIB loading example code, you may notice that an argument there was called owner
and I just passed a self-reference to it. In modern Obj-C and Swift, the self-retain is released the moment the owner is destroyed. If there are no more strong references to the window when that happens, the window is released as well. But be careful: By default, the owner of the main window is the application delegate, and the application delegate is never destroyed. So if you lose all strong references to the main window and the main window is not set to release on close, it will actually leak.
Finally, there is one more thing that most people miss. The documentation says:
Release when closed, however, is ignored for windows owned by window controllers.
So if you are using a window controller, or if your own window controller is a subclass of a window controller, this property has no effect anyway. That's because since MacOS 10.0, window controllers manage the lifecycles of their windows all by themselves and will prevent this setting from having any effect on the window retain count.
If you use a window controller or subclass of it, ignore that this setting even exists. Otherwise you almost certainly want isReleasedWhenClosed
to be true
in modern Obj-C and Swift code and manage the lifetime of the window yourself via strong references. The only reason to disable it is for the case that you (for whatever reason) have a weak reference only to the window and you don't want it to become nil
when the window is getting closed; but then you must make sure that the owner of the NIB files will get destroyed at some point, otherwise the window will leak.
Apple also confirms that the way set isReleasedWhenClosed
is hardly ever the cause of a problem and if you must change that setting to fix an issue, something is broken about your memory management to begin with:
In practical terms, isReleasedWhenClosed is almost never the cause of the problem. It's a self-retain of the window, and setting it true or false just changes an under-release problem into an over-release problem, or vice versa. :)
Note that DTS means "Developer Technical Support" and is Apple's paid developer support (yes, you must pay to get DTS tickets) and the replies from DTS are official replies from Apple by an Apple employee.
Upvotes: 1