Retain/Release of subviews still unclear in viewDidUnload


#1

Hi, I hope you can help resolve my fuzziness regarding what should be released in viewDidUnload (as caused by a Low Memory Warning) and what not.

To my eye, the following view controllers are conceptually identical:
CurrentTimeViewController, where the timeLabel is defined in the xib and connected to the controller there. I can not see any place in the view controller, where the timeLabel is retained explicitly another time. Yet it is claimed in this chapter that the label is retained by the subview (that I can see) AND the view controller. As it is, the code is working after a memory warning. So even though I don’t understand how, I’ll just assume for now, that this is right. But that gives me problems understanding…

… my MapViewController, which is my solution to the challenge and basically the implementation of Whereami put into a controller, with the MapPlace classes and xib included. On its own and without any code in viewDidUnload, this runs just fine. However, going by the rules that seemed to apply to CurrentTimeViewController, I’ll now just go ahead and try to release an UI item in viewDidUnload:

  • (void)viewDidUnload {
    [super viewDidUnload];
    [locationManager release]; // This seems to work, it’s just recreated when needed

    [activityIndicator release];
    }

Go to another Tab, create a memory warning, try to switch back to the map, BOOM!
Program received signal: “EXEC_BAD_ACCESS”

So why doesn’t this work here? I see no conceptual difference in how both view controllers retain their UI items.


#2

The subviews are retained by the view because they are subviews of the view - that part, by your explanation, I believe you know.

The reason the subviews are retained by the view controller as well is because the view controller has IBOutlets to these subviews. When the Nib loader loads a XIB file in, it creates all of the objects and autoreleases them. (The View automatically retains its subviews when it reconstructs the view hierarchy stored in the XIB file.) Then, connections are established between any objects that you have set to have connections. Connections are established by something called Key-Value Coding (which I intend to discuss in the 2nd edition of the book).

The basic idea behind Key-Value Coding is that you can send the message setValue:forKey: to an ANY Objective-C object. setValue:forKey: takes an object (the value) and a key (a string). When executed, this method looks for a setter method named set:. For example, [object setValue:a forKey:@“foo”] looks for a method named setFoo:. If it finds it, it sends setFoo: to the object with the value as the argument. If it does not find it, it actually goes and looks at the instance variables of the object and finds one named foo or _foo. If it finds an instance variable, it releases the old object it points to, retains the new object and assigns foo to point at the new object. (There is also a valueForKey:, which looks for a getter with the same name as the key and barring that, finds a variable with the name of the key or _key.)

Because connections are made with this technique, and you have no setter method for your outlets (which is common), anything you make a connection to in the XIB file gets retained when the XIB file gets opened by the owner of the outlet.

In your example, you shouldn’t release locationManager in viewDidUnload - it is a model object. You should release views in viewDidUnload. (It’s not taking up a lot of memory anyhow.) If you have non-view objects to clean up on a low memory warning, do so by overriding didReceiveMemoryWarning, making sure to call the superclass’ implementation.


#3

OK, I see. Additionally, I printed the retainCounts from these subviews both in the CurrentTimeViewController and the MapViewController and they are in 1 in both cases. Now I believe that everything is being cleaned up nicely. Which leads me to the conclusion that there must be something failing in my MapViewController when it tries to recreate its view hierarchy.

Setting breakpoints shows that viewDidLoad is never even entered again after switching back to the view after a memory warning. At this point, I’ve got no idea what possibly may be going wrong. When the App is terminated, the debugger is in objc_msgSend and displaying assembly that I don’t understand. Interestingly, the frame directly below that is OBJC_IVAR_$_CurrentTimeViewController.timeLabel. If you feel you could figure out something by looking at my MapController, you can find it here: pastie.org/1178699
I’d appreciate any suggestions/explanations.


#4

Thoughts:

In your init method, you send initWithTabBarSystemItem: to your tabBarItem. You shouldn’t do this - don’t send an init message to an object after it has already been created. If you weren’t the one to create it (via alloc), you can’t send it init. If you want one of the stock tab bar items, create a new one, initialize it, and then use [self setTabBarItem:tbi];

viewDidLoad and viewDidUnload should really be all about view objects. Having your location manager created/destroyed in these methods may cause unpredictable results. Instead, either keep your location manager around at all times, or create/destroy it in viewWillAppear:/viewWillDisappear:.

In viewDidUnload, you must nil out the pointers for objects you are releasing. When viewDidUnload gets called, your view controller still exists and therefore it still has pointers to these now-dead objects. So if at any time in the future you try to access these objects before the view is reloaded, you will crash because you are sending a message to a dead object. The rule is, if an object releases an instance variable in any place other than dealloc, then it must assign a new value to that instance variable immediately (either nil or a new object). (You don’t need to set instance variables to nil in dealloc, because those instance variables will disappear along with the object.)

Given the information presented, I would guess your problem is actually in CurrentTimeViewController’s viewDidUnload. If you are releasing timeLabel in that method and not setting it to nil (as you were doing in MapViewController), you might get a crash. When ever you see a crash on objc_msgSend, it essentially means you are sending a message to a deallocated object. Check the next bold-text stack frame above it for the offending message.


#5

Joe, thanks a lot for your reply. I’ve gone through your list of thoughts, implemented them one by one and behold: not setting the object pointers to nil in viewDidUnload was the cause for that crash. Which was a stupid thing to forget, since the CurrentTimeViewController class from the book already shows this. Strange however, that the stack’s information pointed to CurrentTimeViewController (where I set the pointer to nil) rather than MapViewController.