Next article: Friday Q&A 2012-04-27: PLCrashReporter and Unwinding the Stack With DWARF
Previous article: Introducing PLWeakCompatibility
Tags: fridayqna memory nib
I'm back from my hiatus and ready with a fresh journey into the netherworld of Apple's platforms. Today's subject comes from several readers who suggested that I discuss the subtleties of dealing with memory management and nibs, and particularly the differences between the Mac and iOS.
Nib Loading Overview
When you load a nib, two important steps happen in sequence. First, the loader instantiates all of the objects in the nib. Second, it connects all of the outlets specified in the nib.
When it comes to memory management, there are two relevant areas. The first is how to properly manage outlets. The second is how to manage the top-level objects in the nib. A nib contains a hierarchy of objects, where each object is owned by its parent, but the objects at the top level of that hierarchy are a special case.
Mac Nib Loading
Let's talk about how nib loading works on the Mac, with respect to the two relevant memory management areas.
Top level objects are instantiated using alloc
and init
(or a class-specific initializer like -initWithContentRect:styleMask:backing:defer:
for NSWindow
instances. They are then left like this, with responsibility for finally releasing them implicitly transferred to the File's Owner object. If you use NSWindowController
or NSViewController
to load the nib, it automatically takes ownership of these objects and will release them when the controller is destroyed.
To set an outlet, the nib loader first searches for a setter method. If the outlet is called foo
, the loader searches for a method called setFoo:
. If such a method exists, the loader calls it, passing the value of the outlet as the parameter.
If no such method exists, the loader searches for an instance variable with the same name as the outlet. If it finds such an instance variable, the loader sets its value directly to the outlet value without performing any memory management.
Finally, if no method and no instance variable can be found, the outlet connection fails and the outlet is not set.
iOS Nib Loading
Now let's talk about how nib loading works on iOS. Overall it's very similar, but there are subtle differences. Don't worry about trying to find all of the differences, as I'll point them out and analyze them afterwards.
Top level objects are instantiated using alloc
and init
(or a class-specific initializer), and then autoreleased. In the absence of anything else retaining them, these objects will be automatically destroyed.
To set an outlet, the nib loader calls -setValue:forKey:
with the outlet value and outlet name. The Key-Value Coding machinery then takes over and searches for a way to set that particular key. For an outlet called foo
, it will first search for a method called setFoo:
. If such a method exists, it calls that method, passing the value of the outlet as the parameter.
If no such method exists, the KVC machinery searches for an instance variable called _foo
, _isFoo
, foo
, or isFoo
. If any of those exists, it sets the first one it finds to the value of the outlet, releasing the old value in the instance variable (if any) and retaining the new value.
If no method and no instance variable are found, KVC calls setValue:forUndefinedKey:
. By default, this raises an execption, and it can be overridden to implement custom behavior for unknown keys.
The Differences
These two systems are similar but not quite the same. The differences are due to the more modern nature of iOS. Without exception, where the two systems differ, the iOS way is more sensible. Unfortunately, the Mac way can't be changed without severely breaking backwards compatibility. These differences are:
- Top level objects must be explicitly released on the Mac, unless you use an
NSWindowController
orNSViewController
to load the nib. On iOS, they are autoreleased. - Directly setting the ivar on iOS retains the outlet due to how KVC works. On the Mac, the outlet is not retained.
- Because directly setting the ivar results in a retain on iOS, such outlets must be released in
dealloc
. On the Mac, they can just be ignored there. - Directly setting the ivar on iOS has a more thorough search pattern than on the Mac, due to KVC.
The Similarities
Keeping track of these differences is mentally taxing and error-prone, especially if you switch between the two platforms. Getting it wrong can cause a leak or a crash (or both). The best way to handle the differences is to stick to areas where the two platforms are the same. Fortunately, those areas are also the most convenient and best ways to approach nibs anyway.
When loading nibs with a Cocoa controller class (NSWindowController
and NSViewController
on the Mac, UIViewController
on iOS), top-level objects in the nib are automatically handled for you, and thus the behavior becomes the same on both platforms in this case. It's extremely rare to need to load a nib directly, and if you find yourself doing it, you should probably stop and use one of these controllers instead.
When using @property
for outlets, memory management is consistent across both platforms, since they both use the setter if one exists. The memory management for the property can be set as you like, although strong
or retain
is generally preferred. In that case, you must release
the property value in dealloc
, just as you would with any other strong property, unless you're using ARC. weak
can be a good choice for outlets to subviews on iOS, where the views may be unloaded and you don't want a strong reference to keep them alive behind your back.
A Convenient Table
Here's a full summary of the various situations in convenient table form:
Outlet Type | Mac | iOS |
Directly setting the ivar | Unretained reference, do not release | Retained reference, must release in dealloc |
Strong/retain setter | Release in dealloc (or let ARC handle it) | Release in dealloc (or let ARC handle it) |
Assign/weak setter | Don't need to do anything | Don't need to do anything |
Top-level objects | Use NSWindowController or NSViewController to load nibs, silly |
Use UIViewController to load nibs, silly |
No seriously, what about top-level objects? | Release each top-level object to balance the alloc sent when loading the nib |
Don't do anything |
Conclusion
Nib memory management is similar between Mac and iOS but just different enough to be annoyingly confusing. Fortunately, it's easy to mitigate the confusion by sticking to areas where the two platforms behave identically, which results in best practices anyway. Always use a Cocoa controller to load nibs rather than loading the nib directly yourself. Always declare properties for your outlets. As with any property, if your outlet properties are strong, then you must release the backing instance variable in dealloc
(or let ARC do it for you).
That's it for today. Come back next time for another exciting and tittilating edition of Friday Q&A. Until then, since Friday Q&A is driven by reader suggestions, please send in your ideas for topics to cover.
Comments:
Unless you're using ARC. From https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/LoadingResources/CocoaNibs/CocoaNibs.html#//apple_ref/doc/uid/10000051i-CH4-SW18 :
"If the File’s Owner is not an instance of NSWindowController or NSViewController, then you need to decrement the reference count of the top level objects yourself. With manual reference counting, it was possible to achieve this by sending top-level objects a release message. You cannot do this with ARC. Instead, you cast references to top-level objects to a Core Foundation type and use CFRelease."
(sob)
Does this mean I can have `assign` type properties, in non-ARC environment, to subviews of top level objects when loading nibs on iOS so that I don't need to release them in viewDidUnload and dealloc methods?
Using weak for subviews has the advantage that your viewDidUnload methods hardly have to do any work anymore, since the weak outlets will be automatically unloaded for you when self.view disappears.
However, if the nib contains any other top-level views (that you made strong outlets for), then these properties still need to be "nilled out" in viewDidUnload.
Apple discourages dot syntax in init/dealloc but encourages
self.myOutlet = nil;
in viewDidUnload. What's the best convention, here?
I understand the reasoning behind not using
self.bloop = blah;
within the init and dealloc methods because of the possibility of those setter methods being overridden in a subclass amongst other reasons.
Is it simply for convenience that they encourage
self.outlet = nil;
within the boilerplate comments on every UIViewController's viewDidUnload method? What's your preferred method and why?Andrew Bonventre: The reasons for being dubious about calling setters in init/dealloc is not simply because the setters may be overridden, but because you'd end up calling those overridden setters on a partially constructed/destructed object. That's not a problem in something like viewDidUnload. In fact, not only is it acceptable to call the setter there, it's really what you should do: screwing with ivars directly instead of using their accessors is generally a big no-no. init/dealloc are exceptions to this rule, but viewDidUnload definitely is not.
Rob C. Grant: Yes, it's fine to use a UIViewController and then do stuff with its view. You'll probably want to subclass UIViewController to add code to control the reusable view element, then keep that controller around and manipulate it from your higher level controller to fiddle with the view in question.
Since I'd consider getting rid of the view without disposing of the view controller on the Mac as very uncommon, (and I think the absence of a viewDidUnload is a pretty good indicator for that) I don't really understand how he arrived at that conclusion anyways...
'When the NIB gets loaded, 'viewDidLoad' method gets called... and... if for whatever reason you need to force the ViewController to load a nib file during the init... the common hack is to simply call [self view]'.
I don't like that practise, but it has been handy in several occasions.
Thanks for sharing this!
If you got some top-level objects in your MainNib, they are not autoreleased and will remian in memory except you release them explicitly.
In some circumstance, this may cause a leak.
Comments RSS feed for this page
Add your thoughts, post a comment:
Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.
There are two ways, either the type of object is known by IB (you didn't pull a NSObject cube into the nib), then the object is always initialized using -initWithCoder:, if the object is not known, then it uses -init only.