Thursday, October 14, 2010

Outlets, Cocoa vs. Cocoa Touch

I almost always follow Apple's lead on Cocoa and Cocoa Touch conventions. I figure that by the time outside developers like me see something for the first time, Apple engineers have been living with that thing for many months, so they've likely got a much better idea than I do about the best way to use that new thing.

But, after spending time with their stuff, sometimes — not often, but sometimes — I disagree with what appears to be Apple's recommended "best practice" for doing something. I think I've come to the decision that the IBOutlet behavior in iOS is one of these areas.

If you look at Apple's documentation snippets and sample code, you find that they almost always retain IBOutlet properties, like:


#import <UIKit/UIKit.h>


@interface FooView : UIView 
{

}
@synthesize (nonatomic, retain) IBOutlet UIButton button;
@synthesize (nonatomic, retain) IBOutlet UITextField textField;
@synthesize (nonatomic, retain) IBOutlet UIImageView imageView;
@end


There's a good reason for this. In iOS, the documentation explicitly states that you need to retain all outlets because the bundle loader for iOS autoreleases all objects created as a result of loading a nib.
Objects in the nib file are created with a retain count of 1 and then autoreleased. As it rebuilds the object hierarchy, however, UIKit reestablishes connections between the objects using the setValue:forKey: method, which uses the available setter method or retains the object by default if no setter method is available. If you define outlets for nib-file objects, you should always define a setter method (or declared property) for accessing that outlet. Setter methods for outlets should retain their values, and setter methods for outlets containing top-level objects must retain their values to prevent them from being deallocated. If you do not store the top-level objects in outlets, you must retain either the array returned by the loadNibNamed:owner:options: method or the objects inside the array to prevent those objects from being released prematurely.

This is different from Cocoa on the Mac, where it wasn't necessary to retain outlets and people rarely did. In fact, we didn't usually bother with accessor or mutator methods for outlets (it was just unnecessary extra typing in most cases), we just put the IBOutlet keyword in front of the instance variable and the nib loader was happy to attach our outlets like that, retaining the objects that needed retaining.

The behavior under Cocoa/Mac is not actually to retain everything in the nib, but rather, to retain any object that doesn't have a parent object to retain it. So, in other words, if an object in a nib will be retained by something else, like a superview, the nib loader doesn't bother to retain it again. But, if it doesn't, the bundle loader retains it so that it doesn't get deallocated.

This is a logical approach and, in fact, was necessary back in the pre-Objective-C 2.0 days because outlets back then were just iVars and there was no easy way for the controller class to retain objects that needed to be retained.

I have to wonder why they would change the fundamental behavior of a foundation object like NSBundle between Mac OS and iOS? NSBundle is not part of Cocoa or Cocoa Touch, it's part of Foundation, and the whole point of Foundation is to have common objects between the different operating systems.

I wrote a small project to test if the Bundle Loader really did behave differently, as documented, by using an instance of UIView in the nib with no superview. Sure enough, when I didn't retain the outlet, I either got an EXC_BAD_ACCESS or a different object altogether when I printed the outlet to NSLog(). The difference is real. The bundle loader on the Mac will retain outlets for you if they need to be retained, which allows you to continue using instance variables, or properties with the assign keyword. This means you don't have to release your outlets in dealloc and you don't have to mess around with anything like viewDidUnload on iOS.

The bundle loader on the iOS, on the other hand, does not retain anything for you, so if an object does not have a parent object to retain it, you have to retain it in your controller class or you will end up with an invalid outlet.

I really don't see the value in changing this behavior. I'm guessing the decision was made for the sake of memory efficiency in the early days of the iPhone. The idea being that you might load a nib with object instances that you aren't actually using, and with the old behavior, those would take up memory as long as the nib was loaded. That doesn't necessarily sound like a good idea on an embedded device with no virtual memory and 128 megs of RAM, which is what the original iPhone and iPhone 3G had.

Despite that, I think the cure here is worse than the disease. If you don't remember to release your outlets in viewDidUnload (which, if I remember right, we couldn't even do in the 2.0 version of the SDK), your outlets will continue to use up memory after the nib is unloaded, obviating any advantage of the lazy loading. Essentially, it's more fragile, because it depends on the programmer doing the right thing and there are few if any situations where a programmer would need to not do the right thing.

By virtue of the bundle loader not retaining outlets, it also requires more rote, boilerplate code to be written in every controller class in every iOS application, yet it runs just as much of a risk of unnecessary memory use, arguably a greater risk. In other words, the cure is no better than the disease.

iPhones are getting more robust and less memory constrained with every new device that comes out. I would argue that it's already time (or, at very least, soon will be time), to bring the behavior of the two bundle loaders together. If they are brought together, they should be brought together using the old Cocoa behavior not the new iOS behavior. When you think of the number of people coding for the iOS now, those extra required dealloc and viewDidUnload lines in every single controller class in every single iOS application are really adding up to a lot of engineering hours lost on boilerplate.

A few weeks ago, I started experimenting with using assign instead of retain for IBOutlets except in cases where the outlet's object didn't have a superview or another object retaining it. If an outlet's not a view or control, then I also use retain. In essence, I'm mimicking the old behavior of the nib in designing my controller classes.

This has led to a lot less typing and less code to maintain because 95% or more of the outlets I create are connected to objects retained by their superview during the entire existence of the controller class.

Now, I'm not necessarily saying you should do what I'm doing. It can be tricky, at times, remembering which objects need to be retained and they can be hard to debug if you get it wrong. Apple has made a recommendation with good reason and I don't think you should disregard that recommendation lightly. That being said, if you're comfortable enough with Objective-C memory management and the bundle loader to be able to distinguish when a nib object will be automatically retained by something else, you could save yourself a fair bit of typing over time.

I normally try to embrace changes Apple makes, but in this case, I just can't convince myself that this was a good change. The old nib behavior of retaining only things that need retaining has been in use for over 20 years, dating back to when desktop computers were less powerful than our iPhones are, and there doesn't appear to be any practical advantage to the change. On the other hand, we'd all benefit from going back to the old Mac OS bundle behavior because we'd have less make-work to do when setting up a controller class. There's also little danger in changing this behavior because code that follows Apple's current recommendations would continue to work correctly.



22 comments:

Chris said...

Stupid question time.

If you have the code:

@interface Foo : NSObject {
IBOutlet UILabel *foo;
}
@end

Does this fall into the same category?

I ask because it doesn't seem to suffer the issues you've said.

FlorianAgsteiner said...

Theres a Problem in your Approach because After a Lowmemory warning hidden controller views might be released but the outlets contain a Reference so when the View is reloaded all the outlets will be invalid.

The Best Way should Be a custom view As rootview with the nessessary outlets (the can be assign). For typesavety you could use a property:

@property(nonatomic,assign,readonly) Customview* customview

- (Customview*) customview{

return (Customview*) self.view;
}

Greetings, Florian

Pardon typos send from my iPhone

Jeff LaMarche said...

Chris:

That's a good question. I didn't see anything in the documentation about it, but it may be that the bundle loader has heuristics that says if there's no property, then use the old Cocoa behavior. I'll have to experiment.

Florian:

I don't believe so. If there's a low-memory warning, it will unload the xib, not selectively release parts of it, and then when it reloads it, it will re-attach outlets to the newly loaded version and then call viewDidLoad again.

Clement said...

You basically agree with Aaron Hillegass who gave a similar advice last year.

Some consider this dangerous. See for instance
http://blog.lukhnos.org/post/161438203/dangerous-advice

Jeff LaMarche said...

Clement:

I actually didn't recommend it. I said I do it, but I also said there's a reason Apple recommends what they do.

My goal is to get Apple to change the behavior, not to get people to use weak references. I want Apple to change the system behavior so that we don't have to do all that silly boilerplate code that's accomplishing nothing.

I do use weak references now in some situation, but I'm comfortable enough with the way the bundle loader and memory management work to feel safe, and I'm sure Aaron's way more comfortable than I am. But I do see the danger for less experienced coders, which is why I put the caveat in.

Daniel said...

One advantage of explicitly nullifying outlets in viewDidUnload is that you can message those nils with abandon without fear of crashing. An example is if you use KVO to update a UILabel whenever a model string changes.

Having said that, it's even better to register for KVO and other notifications in viewDidLoad then unregister in viewDidUnload, which avoids this issue.

Florent Pillet said...

Jeff,

I developped an alternative technique that requires some care too, but at least automates things and saves most of the typing.

Look at http://gist.github.com/270266

I use a common base class for all my UIViewController subclasses, which takes care of properly releasing and nullifying all outlets by automatically finding them using the ObjC runtime.

The only extra step you need to take is to make sure that before calling [super dealloc], you properly released and nullified all non-UIView derived ivars in your class. I find it a small price to pay compared to the time and lines of code spared.

This way, I don't even need to declare/synthesize properties. This technique may not be for everyone, but I find it worth it for my own use.

Igor said...

Or something like this in the interface:

@property (nonatomic, retain, release) NSString *someName;

And later in dealloc:

- (void)dealloc
{
...[super releaseProperties];

...[super dealloc];
}

... where -releaseProperties does the magic.

Darren Stone said...

If you use IBOutlet with your ivars then these references are automatically retained and must be released in viewDidUnload.

If you use IBOutlet with a property then your properties must follow basic Cocoa memory management practices. Your ivars must still be released in viewDidUnload.

If you change your properties to "assign" rather than "retain" then you're not following Cocoa memory management guidelines and you simply can't expect that to work.

Tarlen said...

I've stumbled upon this issue before, but it's a bit deeper than you realise.

The documentation says (and of course I can't find it again) that outlets are retained automatically upon setting IF there is no setter method defined.

So in the first commenter's example, the label WILL be retained automatically. The harder bit to see is that you still need to release it.

If you have a setter method, either explicitly or via synthesize, then it's up to you to retain the outlet or not, as you choose.

eridius said...

I think you're very confused about how this works. There's only 2 differences between the memory management for nibs on OS X vs iOS. The first, is that all ivar IBOutlets on iOS get retained by default, but on OS X they don't. This is due to iOS using setValue:forKey: explicitly (which retains the object if there's no setter for it) but OS X using a different mechanism. The second difference is OS X automatically retains all top-level objects in the nib, but iOS doesn't. The only reason OS X automatically retains these objects is because outlets to ivars are otherwise not automatically retained, but top-level objects, by definition, have no parent to keep them alive.

So the way it works on the desktop is all your outlets to objects in the nib work because their parent keeps them alive, and the top-level objects in the nib always stay alive. This means that when you're cleaning up after yourself, you need to remember which outlets to release (the ones pointing to top-level objects) and which ones to simply nil out (all other outlets).

However on iOS the behavior has been standardized. Simply release all your outlets.

If you follow recommended practice and use @properties for all of your outlets, then the only thing you have to worry about is the top-level objects on OS X. Of course, if you use NSNib to load your nib then you can go through and release all the top-level objects right away, thus ensuring that only your outlets are holding them alive and restoring compatibility with iOS. The only remaining quirk has to do with -awakeFromNib, but that's a very minor point.

I should also mention that you can't share nibs between OS X and iOS anyway, so there's no pressing need to have a single codebase that handles nibs on both OS X and iOS. But if you use @properties for all your outlets and just be sure to release all top-level objects on OS X, then you really have nothing to worry about.

For the record, behavior did not change because of memory efficiency in early iOS. It changed because the OS X model is simply broken. The difference in handling of memory between top-level objects and any other object is a PITA and I'm sure causes a great many desktop apps to accidentally leak top-level objects. The iOS model is simple and consistent. As for releasing them in viewDidLoad, you need to nil out your outlets anyway, or you run the risk of dereferencing a garbage object, and if you use properties for everything then it becomes as simple as saying `self.foo = nil;` In fact, if you declare your properties as assign (which is a convenient way of restoring OS X memory management behavior), you'd use the exact same code to nil them out. And in iOS 2.0 you could do the exact same thing in -setView: (if view is nil).

Your assertion that using assign for properties means "95% or more of the outlets I create are connected to objects retained by their superview during the entire existence of the controller class" is wrong. If a memory warning comes along and your view is released, you're now holding onto a bunch of garbage objects, and referencing any one of them is likely to cause a crash. So even in this situation you want to nil them out in -viewDidUnload.

One last thing. @schwa already mentioned this, but the nib-loading methods in NSBundle are actually a category on top of it, which comes from AppKit or UIKit. And the NSNib/UINib classes are also in AppKit/UIKit.

eridius said...

I think you're very confused about how this works. There's only 2 differences between the memory management for nibs on OS X vs iOS. The first, is that all ivar IBOutlets on iOS get retained by default, but on OS X they don't. This is due to iOS using setValue:forKey: explicitly (which retains the object if there's no setter for it) but OS X using a different mechanism. The second difference is OS X automatically retains all top-level objects in the nib, but iOS doesn't. The only reason OS X automatically retains these objects is because outlets to ivars are otherwise not automatically retained, but top-level objects, by definition, have no parent to keep them alive.

So the way it works on the desktop is all your outlets to objects in the nib work because their parent keeps them alive, and the top-level objects in the nib always stay alive. This means that when you're cleaning up after yourself, you need to remember which outlets to release (the ones pointing to top-level objects) and which ones to simply nil out (all other outlets).

However on iOS the behavior has been standardized. Simply release all your outlets.

If you follow recommended practice and use @properties for all of your outlets, then the only thing you have to worry about is the top-level objects on OS X. Of course, if you use NSNib to load your nib then you can go through and release all the top-level objects right away, thus ensuring that only your outlets are holding them alive and restoring compatibility with iOS. The only remaining quirk has to do with -awakeFromNib, but that's a very minor point.

I should also mention that you can't share nibs between OS X and iOS anyway, so there's no pressing need to have a single codebase that handles nibs on both OS X and iOS. But if you use @properties for all your outlets and just be sure to release all top-level objects on OS X, then you really have nothing to worry about.

For the record, behavior did not change because of memory efficiency in early iOS. It changed because the OS X model is simply broken. The difference in handling of memory between top-level objects and any other object is a PITA and I'm sure causes a great many desktop apps to accidentally leak top-level objects. The iOS model is simple and consistent. As for releasing them in viewDidLoad, you need to nil out your outlets anyway, or you run the risk of dereferencing a garbage object, and if you use properties for everything then it becomes as simple as saying `self.foo = nil;` In fact, if you declare your properties as assign (which is a convenient way of restoring OS X memory management behavior), you'd use the exact same code to nil them out. And in iOS 2.0 you could do the exact same thing in -setView: (if view is nil).

Your assertion that using assign for properties means "95% or more of the outlets I create are connected to objects retained by their superview during the entire existence of the controller class" is wrong. If a memory warning comes along and your view is released, you're now holding onto a bunch of garbage objects, and referencing any one of them is likely to cause a crash. So even in this situation you want to nil them out in -viewDidUnload.

One last thing. @schwa already mentioned this, but the nib-loading methods in NSBundle are actually a category on top of it, which comes from AppKit or UIKit. And the NSNib/UINib classes are also in AppKit/UIKit.

eridius said...

I think you're very confused about how this works. There's only 2 differences between the memory management for nibs on OS X vs iOS. The first, is that all ivar IBOutlets on iOS get retained by default, but on OS X they don't. This is due to iOS using setValue:forKey: explicitly (which retains the object if there's no setter for it) but OS X using a different mechanism. The second difference is OS X automatically retains all top-level objects in the nib, but iOS doesn't. The only reason OS X automatically retains these objects is because outlets to ivars are otherwise not automatically retained, but top-level objects, by definition, have no parent to keep them alive.

So the way it works on the desktop is all your outlets to objects in the nib work because their parent keeps them alive, and the top-level objects in the nib always stay alive. This means that when you're cleaning up after yourself, you need to remember which outlets to release (the ones pointing to top-level objects) and which ones to simply nil out (all other outlets).

However on iOS the behavior has been standardized. Simply release all your outlets.

If you follow recommended practice and use @properties for all of your outlets, then the only thing you have to worry about is the top-level objects on OS X. Of course, if you use NSNib to load your nib then you can go through and release all the top-level objects right away, thus ensuring that only your outlets are holding them alive and restoring compatibility with iOS. The only remaining quirk has to do with -awakeFromNib, but that's a very minor point.

I should also mention that you can't share nibs between OS X and iOS anyway, so there's no pressing need to have a single codebase that handles nibs on both OS X and iOS. But if you use @properties for all your outlets and just be sure to release all top-level objects on OS X, then you really have nothing to worry about.

Continued in part 2... (blame Blogger)

eridius said...

Continued from part 1... (blame Blogger)

For the record, behavior did not change because of memory efficiency in early iOS. It changed because the OS X model is simply broken. The difference in handling of memory between top-level objects and any other object is a PITA and I'm sure causes a great many desktop apps to accidentally leak top-level objects. The iOS model is simple and consistent. As for releasing them in viewDidLoad, you need to nil out your outlets anyway, or you run the risk of dereferencing a garbage object, and if you use properties for everything then it becomes as simple as saying `self.foo = nil;` In fact, if you declare your properties as assign (which is a convenient way of restoring OS X memory management behavior), you'd use the exact same code to nil them out. And in iOS 2.0 you could do the exact same thing in -setView: (if view is nil).

Your assertion that using assign for properties means "95% or more of the outlets I create are connected to objects retained by their superview during the entire existence of the controller class" is wrong. If a memory warning comes along and your view is released, you're now holding onto a bunch of garbage objects, and referencing any one of them is likely to cause a crash. So even in this situation you want to nil them out in -viewDidUnload.

One last thing. @schwa already mentioned this, but the nib-loading methods in NSBundle are actually a category on top of it, which comes from AppKit or UIKit. And the NSNib/UINib classes are also in AppKit/UIKit.

Michael said...

Wow I can't tell you how confused I am now.

Been checking my code and it appears I only retain certain IBOutlets and not others. Maybe I've misunderstood from the very beginning but I believed that I only needed to synthesize and retain an object if I had need of a setter method.

i.e self.myobject = [myobject init] alloc];

So take for example a reference to a UITableView added with Interface Builder. I only need an IBOutlet reference so that I can do a [myTableView reloadData]

Do I really need to synthesize and retain it? Reload only ever gets called from the view itself, which means it is being displayed, which means the tableview is not released. Conversely if the view disappears and the OS needs to release it, do I care?

Can someone put me straight on this please.

drops said...

The reason IMHO is this: on Mac there is sufficient RAM so that Apple could afford to intentionally "leak" objects loaded from nibs. On iOS those might actually be really required to be freed in viewDidUnload if memory runs out.

Hamish said...

With the OS X approach, you need to be aware of which objects in your nib are top-level objects, because you need to release them yourself.

With iOS, the only outlets you *have* to create retained properties for are the top-level objects.
The other outlets you can feel free continue to leave dangling, just as you always did in OS X.

In other words, the iOS approach is the more flexible. You *can* care about differentiating between top-level objects and others if you want to, but you don't *have to* like you do with the OS X approach.

This is the major advantage, and one which you can pry out of my cold, dead hands. There are some areas in which Apple engineers get things wrong, but this is not one of them.

bbum said...

@drops No, there is never *ever* a justification for leaking an object. Not ever and the framework patterns are *not* implemented with "some leaks are acceptable" on any platform.

Clement said...

Jeff, does your test project update the UI according to asynchronous model changes, through some network delegate, KVO, notifications, background import to Core Data or any other non-trivial multithreaded mechanism? I'm asking this because I suspect such updates could crash when sent to a deallocated outlet. Since there's no viewWillUnload method, even viewDidUnload might be too late to nil outlets and turn off those updates. Whereas if you retain outlets, viewDidUnload wouldn't be too late to both release and nil them.

Anyway, while we all hate boilerplate, I'm under the impression it's been reduced quite a bit lately. Modern ABI support in the simulator, synthesized ivars, even auto-synthesized ivars now, those really help. Unless you're in the anti dot notation camp, but that's another debate...

Jeff said...

I actually wrote a short blog post on this recently, yes it's very annoying in the difference in iOS and Mac!!

Joe Conway said...

This article is missing the fundamental point that UINibLoader uses KVC to make connections whereas the nib loading mechanism in Cocoa does not.

Therefore, in iOS, if you make a connection to an ivar, by default, it is a strong reference. Because of how KVC works, you can create a setter method for the outlet and choose how that memory is managed.

A solid understanding of KVC is essential for iOS programming - it is the underlying mechanism to many bigger APIs (Core Data, Core Animation, Nib loading to name a few).

Tharik Sham : Mobile Tricks said...

Nice post!!!!!!!!!!!!

pcmobileshelper.blogspot.com
Pc Mobile Help and Mobile Tricks


mobiletrickspc.co.cc
Mobile-Tricks Home