mikeash.com: just this guy, you know?

Posted at 2012-04-13 14:14 | RSS feed (Full text feed) | Blog Index
Next article: Friday Q&A 2012-04-27: PLCrashReporter and Unwinding the Stack With DWARF
Previous article: Introducing PLWeakCompatibility
Tags: fridayqna memory nib
Friday Q&A 2012-04-13: Nib Memory Management
by Mike Ash  

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:

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 TypeMaciOS
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.

Did you enjoy this article? I'm selling whole books full of them! Volumes II and III are now out! They're available as ePub, PDF, print, and on iBooks and Kindle. Click here for more information.

Comments:

In iOS, top-level objects and other objects are not initialized with a class-specific init.
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.
"Release each top-level object to balance the alloc sent when loading the nib"

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)
You still have to release the objects when using ARC, ARC just makes it really hard to do so. Yet another reason you should always use a Cocoa controller to load nibs instead of doing it yourself.
"weak can be a good choice for outlets to subviews on iOS"

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?
Since top-level objects in iOS nibs aren't retained by the nib loader, they'll be destroyed as soon as everybody stops expressing interest in them. If the only connection to them is an assign or weak outlet, they'll be destroyed immediately once the nib is done loading, probably not what you want.
I guess you omit garbage collection since it's basically deprecated now? Shame, because it would seem to be the easiest case of all. Reading the doc Avi linked to reminds me of how much I'll miss GC! :(
On iOS with ARC, Apple now recommends making weak outlets for all subviews, but any top-level views besides self.view should have strong outlets as there is nothing but these outlets keeping those objects alive.

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.
A tiny digression but this post sparked the question in my brain again...

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?
I'm unclear on the implications of this article re: nibs that contain reusable interface elements. In such a case, is it appropriate to use a UIViewController to load the nib and then add the controller's view as a subview in some other view controller's hierarchy?
Sean M: The situation is really easy under GC: make outlets, use them, and don't worry about it. GC is very nice when it works....

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.
Funnily enough, I've just read Aaron Hillegass's "Real iPhone Crap, Part I" (http://weblog.bignerdranch.com/?p=95) from back in 2009, where he argued that the use of KVC in NIB loading in UIKit (and thus inferring outlets to be strong) was a bad idea — I'd love to know whether or not he changed his opinion about this!

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...
That article is interesting. It strikes me as a knee-jerk reaction from someone who is really used to the way things are done. I think that, taking a step back and looking at things objectively, the iOS way is much, much better. It's totally consistent and works the way you'd expect it to, whereas the Mac way of sort of doing kind of what KVC does but not quite means you end up with two different facilities that both search your objects for setters and ivars and manipulate them, but in subtly different ways.
A small comment i would add to the article would be something like...

'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!
There is actually a special scene in iOS.

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.
Note: with iOS 6 viewDidUnload is now deprecated. I guess if you're using strong for IBOutlet properties and are concerned with low memory conditions, you can still set them to nil in -didReceiveMemoryWarning.
For iOS using ARC, if I set the property as strong for 1 of my subviews, and do not set it to nil in -didReceiveMemoryWarning (or viewDidUnload when it wasn't deprecated), will it leak or cause some kinds of problem? If yes, please explain. If not, then is there any downside for using @property (strong) for those outlets?
Why objects in storyboard that are attached to scene of view controller are deinit with view controller deinit even if they are not view controller's view subviews and are not outlet-ed to the view controller?

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.

Name:
The Answer to the Ultimate Question of Life, the Universe, and Everything?
Comment:
Formatting: <i> <b> <blockquote> <code>.
NOTE: Due to an increase in spam, URLs are forbidden! Please provide search terms or fragment your URLs so they don't look like URLs.
Hosted at DigitalOcean.