Search code examples
iphoneiosmemory-managementuiviewcontrollerios6

iOS6 and automatic view unloading


Update:
To clarify the context of the question; the question is not on how to avoid the problem, but to clarify what the piece of documentation means, as my experiment suggests that the views I was expecting to unload, based on the documentation, are not unloading. I would like to understand if this is a bug, or if I am misunderstanding the documentation. To solve the problem instead, I know that setting the images in viewWillAppear instead of viewDidLoad, and setting the images to nil in viewDidDisappear, is releasing the memory and the app doesn't crash. However, I would like to understand if the memory should have been released with the initial code, as the experiment is to simulate having such view controllers as outlets, and setting your UI images (backgrounds...) in Interface Builder, instead of setting them in code in viewWillAppear...

Original:
I am trying to understand some new aspect of iOS6, as documented in the View Controller Programming Guide:

On iOS 6 and Later, a View Controller Unloads Its Own Views When Desired The default behavior for a view controller is to load its view hierarchy when the view property is first accessed and thereafter keep it in memory until the view controller is disposed of. The memory used by a view to draw itself onscreen is potentially quite large. However, the system automatically releases these expensive resources when the view is not attached to a window. The remaining memory used by most views is small enough that it is not worth it for the system to automatically purge and recreate the view hierarchy.

Let's say I am creating a simple app, with a rootViewController. This rootViewController has a few child View Controllers, all declared as IBOutlets instead of being allocated in code.

@property(nonatomic,strong) IBOutlet ChildViewController *childViewController1;
@property(nonatomic,strong) IBOutlet ChildViewController *childViewController2;
@property(nonatomic,strong) IBOutlet ChildViewController *childViewController3;
@property(nonatomic,strong) IBOutlet ChildViewController *childViewController4;
@property(nonatomic,strong) IBOutlet ChildViewController *childViewController6;
@property(nonatomic,strong) IBOutlet ChildViewController *childViewController7;
@property(nonatomic,strong) IBOutlet ChildViewController *childViewController8;
@property(nonatomic,strong) IBOutlet ChildViewController *childViewController9;
@property(nonatomic,strong) IBOutlet ChildViewController *childViewController10;

The rootViewController has a few buttons, pressing each of those does a simple presentModalViewController operation on each childViewController

-(IBAction)showChild1Action:(id)sender{
  [self presentModalViewController:self.childViewController1 animated:true];
}


Each childViewController has a close button that dismisses the child view controller.

-(IBAction)closeAction:(id)sender{
  [self dismissModalViewControllerAnimated:true];
}


My expectation from the documentation was that the child view controller view objects would be released from memory, as the child view controllers are dismissed.
However I purposely tested with large view objects, and running profile on such an app, the memory usage just keeps growing as each child controller gets presented, and the app eventually crashes after I present child controller #7 or so.

What is your understanding of what has changed in iOS6 in that aspect?

RootViewController.h

#import <UIKit/UIKit.h>
#import "ChildViewController.h"

@interface RootViewController : UIViewController

@property(nonatomic,strong) IBOutlet ChildViewController *level2ViewController1;
@property(nonatomic,strong) IBOutlet ChildViewController *level2ViewController2;
@property(nonatomic,strong) IBOutlet ChildViewController *level2ViewController3;
@property(nonatomic,strong) IBOutlet ChildViewController *level2ViewController4;
@property(nonatomic,strong) IBOutlet ChildViewController *level2ViewController5;
@property(nonatomic,strong) IBOutlet ChildViewController *level2ViewController6;
@property(nonatomic,strong) IBOutlet ChildViewController *level2ViewController7;
@property(nonatomic,strong) IBOutlet ChildViewController *level2ViewController8;
@property(nonatomic,strong) IBOutlet ChildViewController *level2ViewController9;
@property(nonatomic,strong) IBOutlet ChildViewController *level2ViewController10;

-(IBAction)showChild1Action:(id)sender;
-(IBAction)showChild2Action:(id)sender;
-(IBAction)showChild3Action:(id)sender;
-(IBAction)showChild4Action:(id)sender;
-(IBAction)showChild5Action:(id)sender;
-(IBAction)showChild6Action:(id)sender;
-(IBAction)showChild7Action:(id)sender;
-(IBAction)showChild8Action:(id)sender;
-(IBAction)showChild9Action:(id)sender;
-(IBAction)showChild10Action:(id)sender;

@end

RootViewController.m

#import "RootViewController.h"

@interface RootViewController ()

@end

@implementation RootViewController

@synthesize level2ViewController1;
@synthesize level2ViewController2;
@synthesize level2ViewController3;
@synthesize level2ViewController4;
@synthesize level2ViewController5;
@synthesize level2ViewController6;
@synthesize level2ViewController7;
@synthesize level2ViewController8;
@synthesize level2ViewController9;
@synthesize level2ViewController10;


- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}

-(IBAction)showChild1Action:(id)sender{
    self.level2ViewController1.index=0;
    [self presentModalViewController:self.level2ViewController1 animated:true];
}
-(IBAction)showChild2Action:(id)sender{
    self.level2ViewController2.index=1;
    [self presentModalViewController:self.level2ViewController2 animated:true];
}
-(IBAction)showChild3Action:(id)sender{
    self.level2ViewController3.index=2;
    [self presentModalViewController:self.level2ViewController3 animated:true];
}
-(IBAction)showChild4Action:(id)sender{
    self.level2ViewController4.index=3;
    [self presentModalViewController:self.level2ViewController4 animated:true];
}
-(IBAction)showChild5Action:(id)sender{
    self.level2ViewController5.index=4;
    [self presentModalViewController:self.level2ViewController5 animated:true];
}
-(IBAction)showChild6Action:(id)sender{
    self.level2ViewController6.index=5;
    [self presentModalViewController:self.level2ViewController6 animated:true];
}
-(IBAction)showChild7Action:(id)sender{
    self.level2ViewController7.index=6;
    [self presentModalViewController:self.level2ViewController7 animated:true];
}
-(IBAction)showChild8Action:(id)sender{
    self.level2ViewController8.index=7;
    [self presentModalViewController:self.level2ViewController8 animated:true];
}
-(IBAction)showChild9Action:(id)sender{
    self.level2ViewController9.index=8;
    [self presentModalViewController:self.level2ViewController9 animated:true];
}
-(IBAction)showChild10Action:(id)sender{
    self.level2ViewController10.index=9;
    [self presentModalViewController:self.level2ViewController10 animated:true];
}

@end

ChildViewController.h

#import <UIKit/UIKit.h>

@interface ChildViewController : UIViewController

@property(nonatomic,weak) IBOutlet UIImageView *image1;

@property NSInteger index;

-(IBAction)closeAction:(id)sender;

@end

ChildViewController.m

#import "ChildViewController.h"

@interface ChildViewController ()

@end

@implementation ChildViewController

@synthesize image1;
@synthesize index;


- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    if(self.index==0)
        [self.image1 setImage:[UIImage imageNamed:@"IMG1.JPG"]];
    if(self.index==1)
        [self.image1 setImage:[UIImage imageNamed:@"IMG2.JPG"]];
    if(self.index==2)
        [self.image1 setImage:[UIImage imageNamed:@"IMG3.JPG"]];
    if(self.index==3)
        [self.image1 setImage:[UIImage imageNamed:@"IMG4.JPG"]];
    if(self.index==4)
        [self.image1 setImage:[UIImage imageNamed:@"IMG5.JPG"]];
    if(self.index==5)
        [self.image1 setImage:[UIImage imageNamed:@"IMG6.JPG"]];
    if(self.index==6)
        [self.image1 setImage:[UIImage imageNamed:@"IMG7.JPG"]];
    if(self.index==7)
        [self.image1 setImage:[UIImage imageNamed:@"IMG8.JPG"]];
    if(self.index==8)
        [self.image1 setImage:[UIImage imageNamed:@"IMG9.JPG"]];
    if(self.index==9)
        [self.image1 setImage:[UIImage imageNamed:@"IMG10.JPG"]];

}

- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}

-(IBAction)closeAction:(id)sender{
    [self dismissModalViewControllerAnimated:true];
}

When using Instruments I get these observations:

  • The memory allocation instrument is showing what I would expect to see: total allocation that goes up when child view controller is presented, goes down when child view is dismissed This is after showing then dismissing 3 child view controllers

  • However the activity indicator is telling a different story, with real memory increasing with every presentModalViewController, and never decreasing when dismissing them

This is also after showing/dismissing 3 child view controllers


Solution

  • The answer I eventually got back from Apple:

    Your "large resources" happen to be images that you've loaded cached via +imageNamed:. Because they are loaded cached, they are exempted from the automatic cleanup. Generally only content generated via -drawRect: or by Core Animation is automatically released here. Because your views continue to exist, they continue to hold a reference to these cached images, and we can't purge them on memory warning either

    Looks like cached resource isn't part of the automatic cleanup mentioned in the doc, and those resources only get deallocated when they stop being referenced.