View Controller Lifecycle in iOS 6
by thejoeconwayblog
Some of you may have noticed that your view controllers no longer get sent viewWillUnload or viewDidUnload in iOS 6. That’s because your view controllers don’t automatically unload their views anymore.
Your first thought may be, “Ok, well how do I manually unload my views on a low memory warning? This seems like a step backwards.”
Then you go searching for an answer, and you come up with this:
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
if([self isViewLoaded] && ![[self view] window]) {
[self setView:nil];
}
}
However, this is both unnecessary and potentially harmful because of some changes to the underlying support for UIViews. Therefore, in practice, you rarely need to unload a view controller’s view during a low memory warning. (And in theory, you will never have to.)
But why? If you’ve read our entire iOS programming book, you’ve gained the knowledge that a UIView is a subclass of UIResponder (so that it can receive events) and has a pointer to its own CALayer instance (so that it can be drawn on the screen).
A CALayer is a container for a bitmap image. When a UIView runs its drawRect: method, it is creating the bitmap image for its layer. The rest of a layer’s instance variables (many of which it borrows from its UIView, like its frame and backgroundColor) indicate how and where that bitmap image is composited onto the screen.
But the biggest part of the layer (in terms of memory usage) is that bitmap. A layer itself is only 48 bytes and a standard UIView is only 96 bytes, regardless of their size on the screen. The memory consumption for a layer’s bitmap, however, grows very large depending on the bounds of the layer. For example, a full screen retina-display iPad view can be up to a whopping 12 megabytes.
The approach taken by iOS 6 is that a low memory warning should only destroy the layer’s bitmap, but keep the CALayer and UIView objects intact. This makes sense given the memory consumption of the view and layer are relatively small compared to the bitmap. Additionally, a bitmap can be redrawn by having the view run drawRect: again, so it is not like we’ve lost anything.
In fact, something is gained by this approach: your controller no longer needs to re-populate view content after a low memory warning. For example, consider a view controller that maintained a couple of text fields. If that view controller’s view were to go off screen and a low memory warning were to occur, the view controller must save the text currently in those text fields and repopulate them in viewDidLoad or viewWillAppear:. This is no longer an issue because the text fields are never destroyed, thus they retain their text for when the text field’s bitmap is drawn again. This simplifies a very error-prone section of UIViewController code.
There are some additional smarts built into this process of dumping the layer’s bitmap image that are really cool. First, let’s make sure we understand allocation and deallocation. When a memory is allocated (an object, a bitmap, whatever), an appropriately sized chunk of memory from the heap is marked as “in use” and a pointer to the beginning of that chunk of memory is returned. In the case of an object allocation, we think of that pointer as “the object” but really, it’s just the address of a chunk of “in use” memory.
While a chunk of memory is allocated (“in use”), there are guards that prevent code from using that memory unless it is accessed by going through the pointer that was returned during allocation. In a sense, that memory is safe from being screwed with unless you intend to screw with it.
Deallocation simply removes those guards and marks that chunk of memory as “not in use”. This means that the next time you allocate an object, you can use some or all of that “not in use” memory to form a new allocation. Upon deallocation, the values stored in that chunk of memory do not change. While it is possible to access that deallocated memory again and it may be the same, chances are that the memory has changed in some way. Therefore, it’s never safe to access deallocated memory through the pointer you were given upon allocation.
Now, on to why this matters for views and their layers. Each layer has a contents property that points to an object that represents the layer’s bitmap. The type of this object is a private, opaque class named CABackingStore, which contains the actual bitmap as well as some metadata (like whether or not the image has an alpha channel and how many bytes per pixel are used). Thus, it is the CABackingStore that needs to be destroyed when memory is running low.
However, the neat trick here is that a low memory warning does not destroy a CABackingStore. Instead, if a view is offscreen during a low memory warning, its layer’s CABackingStore is set to “volatile”. The volatile flag (which is not the same as the volatile keyword in C), in this context, is like deallocation in the sense that it allows the memory of the CABackingStore to be allocated for a different purpose. The difference between marking this memory as volatile and deallocating it is that the volatile memory can actually be reclaimed; it is not lost forever.
Consider why this is such an interesting optimization. If a view (more accurately, its layer) were to destroy its backing store, the next time the view went on screen it would have to recreate that backing store by running drawRect:. drawRect: is an expensive call, so avoiding it is very important. By allowing the backing store to be reclaimed, the layer can avoid having its UIView execute drawRect: again.
Of course, it is possible that some time between when the backing store was marked as volatile and the view that owned it goes back on the screen, the memory for that backing store was reallocated for a different purpose. In that case, the view would have to run its drawRect: method again to produce its bitmap.
[...] I previously wrote about breaking the old pattern of writing viewDidUnload. The other half of that is the new reality, which Joe Conway’s written about in View Controller Lifecycle in iOS 6. [...]
Excellent explanation… Makes sense to me. Just one thing I am curious to know about
What about custom views that retain some data like some UIImages for example?
In this case I will need to deallocate the view or its images manually, I am correct?
In that situation, you would want to have the view unload its images but not necessarily destroy the whole view. That’s where the “in practice” and “in theory” distinction comes in: in theory, you should be able to write a view that would dump its large data and recreate it when needed. However, in practice, it may just be easier to kill off the view and recreate it.
Excellent article! The end triggered some subtle head scratching algorithms though.
1) Does the view controller’s view call -drawRect: automatically if the volatile memory of it’s layer’s backing store became dirty during a low memory warning?
2) Does this logic propagate to sublayers of the view’s layer? Do their backing stores become volatile as well?
Thanks for the post Joe, really good reading!
@taptanium
1. I’m guessing that once the backing store becomes dirty, the layer knows to redraw when view needs to come back on screen, but I haven’t put together a sufficient enough test for that. It would require allocating memory from the area where the backing store was.
2. Yes, this behavior is actually built into CALayer and not into the view controller. Perhaps I will write a post on how I came to this conclusion, but essentially, when a layer is removed from its superlayer, it gets a callback (contents_visibility_changed) at which point it sets its volatile flag.
Well done! Thanks!
Joe,
does this mechanism with marking as volatile work for all created UIView that are not on the screen or only for UIViewController’s view?
I’m asking myself the same, I also wrote a question on SO, but none answered http://stackoverflow.com/questions/13025939/viewdidunload-deprecation-but-what-about-strong-references-to-secondary-view
Yeah, it does. I’ll have to write up my observations to show why.
[...] View Controllers in iOS6 View Controller Lifecycle in iOS 6 [...]
Joe,
Nice post, thanks for sharing! It inspires me a lot!
After reading your post, I have two questions:
- 1. Since CABackingStore is only set to “volatile”, which means memory is not really freed, so the memory usage shown in Instruments doesn’t lower down while receiving low-memory warning. Am I right?
- 2. If my project is going to be both iOS 5 and 6 compatible, I should not do any subviews-niling stuff in “didReceiveMemoryWarning” and set the subviews to nil in “viewDidUnload”. Right?
1. I’m not sure, actually. I’d certainly like to find out when I have a moment to test it. If you get a chance to test it before I, make sure you are also watching the VM Tracker instrument. Also, I think that removing the view from the view hierarchy set its to volatile and a low memory may or may not trash them once in that state. I still have a lot more investigation to do, but wanted to get this out there to see if anyone else could bring in some more knowledge.
2. Yes, although the preferred method is to make outlets weak and not implement viewDidUnload at all for iOS 5.
Thanks for your quickly reply!
I’m new to iOS development, and I’m wondering that is it possible for your statement 2.
Sure I can make outlets weak while using IB to create my interface. But sometimes I need to create my views and/or controls in programming.
How can I achieve your statement 2 under this condition?
Nelson – use a strong local variable and then move it into a weak instance variable:
__weak UIButton *ivar;
- (void)loadView
{
…
UIButton *btn = …;
[[self view] addSubview:btn];
ivar = btn;
}
[...] View Controller Lifecycle in iOS 6 [...]
Excellent and head scratching article! Thanks for the post. :]
[...] View Controller Lifecycle in iOS 6 Due parole sul nuovo ciclo di vita di un View Controller in iOS 6 [...]
[...] I have seen “great” and logical answers around this topic, but apparently they were proven wrong. Proceed with caution, this topic is very confusing as you [...]
Thanks! Very helpful, I’ve searched far and wide.
Good post , and a very nice sharing ! And I have some question that i can’t understand about the “offscreen view” . As far as i see , it may be consider as :
1.Root view of ViewController that has be removed from view hierarchy by calling the removeFromSuperview method , but hasn’t been set nil .
2.Root view of ViewController that is currently in the view hierarchy , but has been set its bounds so that it move outside the screen viewport .
I am new for IOS , and forgive my poor english !Thanks!
More likely a view controller in a tab bar, but another tab is selected. Or a view controller in a navigation controller deeper in the stack.
Really nice article.
Smart tricks for optimizing memory management, Now I understand why drawRect is not always called.
Thanks.
Simply awesome explanation of what lies underneath…thanks man