Kamis, 14 Oktober 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.

0 komentar:

Posting Komentar

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Lady Gaga, Salman Khan