Custom UIViewControllers, Their Views, and Their Memory Management   4 comments


Download ExampleDifficulty: easy moderate challenging

UIViewControllers serve as backbones in your iPhone or iPad application. Although they are in charge of many other aspects, including user input and output, a task that is somewhat being overlooked is managing memory. In this article, we’ll see how a UIViewController is in charge of its own and its views’ memory footprint, when and how the system can reclaim the memory, and how to control the process. Although this post will benefit all programmers, it relates more to those who manually code their view hierarchy or parts of it, as oppose to relying on Interface Builder and nib/xib files for the job; as your app grows in code size and you learn to write more reusable code, the more relevant this post becomes.

 

Before we look into its internals, let’s try to understand the purpose and place of UIViewControllers in our apps; but for that, we should first understand how view components function in our apps.

 

Everything that is displayed on our screen is a UIView, one of its official subclasses, or a custom subclass that we code ourselves; these are called view components. Their purpose is to look pretty and to help the user achieve whatever your app tries to accomplish, whether by inputting data from the user or outputting data to him or her. A typical screen might contain dozens of views, such as buttons, labels (=texts), images, input boxes, etc. And although every single component works as expected of it, we need a separate element to make them all work in concert – a UIViewController.

 

The interaction between a UIViewController and its views is quite complicated and involves: creating the views and initializing them before their presentation, receiving events from the views upon interaction and changing the views accordingly, and later disposing of them. All of these tasks comprise the business logic of your display. The middle task: “receiving events from the views upon interaction and changing the views accordingly”, is a task that is of interest for developers who use Interface Builder as well. As such, people usually learn how to make it work quite fast; we’ll disregard this task for the rest of this article.

 

So, we’re focused on view controllers’ memory management: “creating the views and initializing them before their presentation” and “later disposing of them”. Since a single view controller is in charge of a single screen worth of objects, an app might have many of such coexisting; while only one is active – currently being displayed on screen – there might be many inactive ones: previous screens which the user traversed in order to reach the active screen, or other tabs that are not visible at the moment. Because of the iPhone’s limited memory, a UIViewController must be able to purge some, or even most, of its memory footprint when it’s not active. To that effect, UIViewControllers allow loading and unloading their display objects. As we find time and again, Apple did a marvelous job in decoupling the operation from its actions; you’d feel the same by the end of this article. Nevertheless, the purging of some of its memory should not change the behavior of a view controller; it should maintain enough information to restore itself in full, as if its memory wasn’t purged at all.

 

When a user first reaches a new screen, UIKit’s lowest-level view manager, UIWindow, requests the new UIViewController’s view object; this view object is an agent to the entire screen’s worth of view objects; the one references all other view components (i.e. sub-views) in its view hierarchy. This is all done automatically in UIKit’s loading process. It all happens in a few lines, as shown by a template project:

– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {   

    // Add the view controller’s view to the window and display.

    [self.window addSubview:viewController.view];

    [self.window makeKeyAndVisible];

    return YES;

}

 

From this excerpt, it seems that you should supply the view in your custom property getter method, whenever you code a custom view controller.  However, that’s not the case; the base UIViewController class intercepts the getter and calls loadView (implement the getter to call loadView), which has to be implemented by you in your custom view controller; by doing so, it gracefully asks your custom view controller class to create and set its view hierarchy to its liking when it needs to. A simple example of a loadView method: 

– (void)loadView {

    UILabel* aLabel=[[UILabel alloc] initWithFrame:[UIScreen mainScreen].bounds];

    self.myLabel=aLabel; // owns this label in a custom property

    [aLabel release]; // de-owning the initial object

   

myLabel.autoresizingMask=UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;

    myLabel.contentMode=UIViewContentModeCenter;

    myLabel.textAlignment=UITextAlignmentCenter;

    [self updateLabelTextAccordingToOrientation];

   

    self.view=myLabel; // allowing UIKit to own the view as well

 }

The last line shows that the view property is being set, as expected. All this process of using loadView seems a bit wonky, but we’ll soon discover its main virtue.

 

After the user navigates back from this screen, UIKit dismisses its reference to the view hierarchy, view, and then calls viewDidUnload to signal you that whatever memory you allocated and owned in loadView can be safely released. In the example above, we took ownership of the label by setting our myLabel property to it. Hence, we should free it in our viewDidUnload method, as shown:

– (void) viewDidUnload {

    self.myLabel=nil;

}

For more information about this directive’s memory management, please advise What are Properties and how do they work?.

 

You might ask, why is this so important? Doesn’t this entire UIViewController going to get deallocated soon, allowing the system to reclaim all its memory anyway? Of course the answer is not so simple. But before we delve into the answer, we should make sure we write the dealloc correctly and release all owned objects, regardless of what we’ll discuss next:

– (void)dealloc {

    self.myLabel=nil;

    [super dealloc];

}

 

But that’s not the entire story. UIKit doesn’t only allow navigation back from a view controller, but also allows navigation to other view controllers from existing ones. In such a case, a new UIViewController will be allocated, and then loaded into view. The old view controller will go off-screen and becomes inactive, but still owns many objects – in our case, the label object, twice actually – once in the custom myLabel property and another in the inherited view property. And so does the new visible view controller, in regard to its view objects. Due to the limited amount of memory of mobile devices, owning the two sets of objects –one in the off-screen view controller and another in the on-screen view controller – might be too much to handle. If UIKit deems it necessary, it can reclaim some of the off-screen view controller’s memory, which is not shown anyway; UIKit knows which view controller is on-screen and which is off-screen, as after all, it is the one managing them (when you call presentModalViewController:animated: or dismissModalViewControllerAnimated:). So, every time it feels pressured, UIKit generates a memory warning, which unloads and releases your off-screen view from the view hierarchy, then call your custom viewDidUnload method for you to do the same, namely to release your myLabel ownership. In the loadView we set two owners for the label: the view property and the custom myLabel property; within the memory warning, UIKit releases self.view automatically, allowing us then to manually release myLabel in our viewDidUnload code. It does so for all off-screen view controllers.

 

In our example, we referenced a single object, a UILabel, for which its memory footprint might be minimal. However, in real-life scenarios, your view hierarchy will probably consist of dozens if not hundreds of UIView components. And until all of their references aren’t properly released, their memory will not be reclaimed. Thus, it’s important to be extremely precise with your views: over-retaining your top view object might prevent it from being reclaimed, and with it, all its many children view objects.

 

UIKit manages all view controllers’ memory consumption at will. For example, let’s consider a user who navigates from view controller A to view controller B and then back to A. It could be that UIKit will unload A’s view when the user navigates away from and then reload it when the user navigates back to A – firing A’s viewDidUnload and another A’s loadView; or, it could be that UIKit will keep A’s view loaded while the user navigates to B and back to A – neither loadView nor viewDidUnload are fired. The decision lies within UIKit, which monitors memory usage. Similar uncertainty will arise if the user navigates from view controller Y back to its creator, view controller X, and then back again – well, actually forward –to Y. Y’s view could be reclaimed and then loaded again, or it might just stay loaded; this will happen only if the creator, X, uses the same Y view controller object it created the first time; however, if a new Y view controller object is being allocated and initialized in X every time X needs Y to be displayed, then Y’s view is guaranteed to be loaded afresh, since loadView  was never called on the new Y object.

 

Due to UIKit’s secretive implementation, you shouldn’t rely on loadView to handle anything besides to load your view, set up whatever is needed for it to display, and link your variables or properties to the view components that you might need to modify later; nor you should rely on viewDidUnload to do anything besides releasing your local variables and properties. Doing so will minimize your bugs and crashes, and will allow your app to run correctly on future iOS releases. Not doing so leads to bugs that are difficult to reproduce and debug: either memory that never gets dealloced, or referencing an object that has been deallocated when a memory warning was fired, causing a crash the next time it is being accessed. It is now quite obvious that decoupling the loading and unloading code from the actual loading process and its decision-making is quite useful when memory is such a constraint.

 

Now, let’s consider the memory warnings by themselves. When the system is running out of memory, it fires a didReceiveMemoryWarning. By default, this method’s implementation in the base UIViewController releases the view and calls viewDidUnload, if it’s not on-screen, as was previously discussed. Off-screen views will be reclaimed and released upon memory warning, but your on-screen view will not get released – it is visible and needed. In case your class owns a lot of memory, such as caches, images, or the like, didReceiveMemoryWarning is where you should purge them, even if they are on-screen; otherwise, your app might be terminated for glutting system resources. You need to override this method to make sure you clean up your memory; just remember you call [super didReceiveMemoryWarning];.

 

Because of the various methods of UIViewController, which relate to memory management, a quick review is useful for when consider what code should go to what method.

 

For setting up, use one of the following:

initWithNibName:bundle:, or your own custom initWithWhatever: (or similar) – where you set up your class data that will be needed for its display later on. This is where you should also do time-consuming initializations, like loading caches.

loadView – where you create your view hierarchy and set it up to match the class data. This is where you should also own references to view components you need for interacting with the user. Should not rely on having all time-consuming data available, as didReceiveMemoryWarning might have been called.

viewWillAppear – happens before the drawing. where you would want to “reset” to your initial state which is extremely important when  the view is reappearing without being loaded again.

viewDidAppear – fired after the view had been loaded, so suitable for start-up animations, and for other timers you need while the screen is shown.

 

For tearing down, use one of the following:

viewDidDisappear – fired after the view had been removed from screen. Where you can stop active timers, such as animations.

viewDidUnload –  fired after the view has been unloaded, so allows you to release ownership of referenced view components.

didReceiveMemoryWarning – fired whether your views are on-screen or off-screen. Should allow you to release less important data, such as the time-consuming data that were set up in init. Must call super if implemented.

dealloc. – Final release of ownership of your class. All previously retained data should be released. Must call super if implemented.

 

All of these methods are implemented in the base UIViewController, so you should call super if you override them. It is imperative in the two highlighted ones.

Advertisements

4 responses to “Custom UIViewControllers, Their Views, and Their Memory Management

Subscribe to comments with RSS.

  1. viewDidUnload does *only* fire when a view unloads because of memory warning. check it yourself and check the apple documents also.

    Jonathan Fanshave
    • This is the de facto situation, I agree, but it doesn’t necessarily will remain so in future iOS releases.

      Apple says “When a low-memory warning occurs, the UIViewController class purges its views if it knows it can reload or recreate them again later. If this happens, it also calls the viewDidUnload method”.

      It doesn’t mean that purging the view and calling viewDidUnload couldn’t happen for other situations. Although not the case in iOS 4.2, at some point in the future these might occur when a view controller has been dismissed and is being torn down and dealloc-ated; or when simply going off screen, for an extremely limited device, for instance.

      Thus, people should not rely that viewDidUnload is coupled with memory warnings, rather than its original better half, the unloading of the view.

  2. Thankx for sharing gr8 detailed info.
    I have one question. Let me explain it to you by example:
    Suppose I have two screens (viewControllers) A and B. On A there is one form to fill up (UITextFields), info link and a submit button. If I press the info link, it will show Screen B.
    Now if I a get memory warning while on B, it will flush the data that I have filled in A. I don’t want to release A’s views.
    How can I achieve this? Should I do something in the -didReceiveMemoryWarning method?

    • If there is not much free memory left on the device, The system has to release A’s views — it’s not for you to decide, but to embrace.
      What you need to do to overcome this situation is not to rely on the views to save the data, but rather to manually save the data into fields (= ivars = instance variables).
      The memory footprint of a string ivar and a UIView is incomparable.

      In A, before you launch B, copy the texts from the text fields into NSString variables/properties.
      Now, if the user goes into B and back without a memory warning, your text fields will contain the old data.
      But if a memory warning comes while on B, the views on A will be reclaimed, and when the user navigates back to A, you will receive a viewDidLoad on A (since it had to be reloaded); so if you override this method, you can reset the text fields content according to the variables.
      Keep in mind that this method is also fired when you first launch this ViewController, so you should probably put the default values for your text fields in the variables.

      Cheers, Ohad.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: