Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Memory management with image saved from UIImagePickerController

I'm writing an app in which the user takes a photo of them self, and then goes through a series of views to adjust the image using a navigation controller. This works perfectly fine if the user takes the photo with the front camera (set as default on devices that support it), but when I repeat the process I get about half way through and it crashes after throwing a memory warning.

After profiling in Instruments I see that my apps memory footprint holds at about 20-25 MB when using the lower resolution front camera image, but when using the back camera every view change adds another 33 MB or so until it crashes at about 350 MB (on a 4S)

Below is the code I'm using to handle saving the photo to the documents directory, and then reading that file location to set the image to a UIImageView. The "read" portion of this code is recycled through several view controllers (viewDidLoad) to set the image I saved as the background image in each view as I go.

I have removed all my image modification code to strip this down to the bear minimum attempting to isolate the problem, and I can't seem to find it. As it stands right now, all the app does is take a photo in the first view and then use that photo as the background image for about 10 more views, allocating as the user navigates through the view stack.

Now obviously the higher resolution photos would use more memory, but what I don't understand is that why the low resolution photos don't seem to be using more and more memory as I go, whereas the high resolution photos continuously use more and more until a crash.

How I am saving and reading the image:

- (void) imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    UIImage *image = [info objectForKey:@"UIImagePickerControllerOriginalImage"];    
    jpgData = UIImageJPEGRepresentation(image, 1);

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);  
    NSString *documentsPath = [paths objectAtIndex:0];
    filePath = [documentsPath stringByAppendingPathComponent:@"image.jpeg"];
    [jpgData writeToFile:filePath atomically:YES];    

    [self dismissModalViewControllerAnimated:YES];
    [disableNextButton setEnabled:YES];

    jpgData = [NSData dataWithContentsOfFile:filePath];
    UIImage *image2 = [UIImage imageWithData:jpgData];
    [imageView setImage:image2];
}

Now I know that I could try scaling the image before I save it, which I plan on looking into next, but I don't see why this doesn't work as is. Maybe I was falsely under the impression that ARC automatically deallocated views and their subviews when they leave the top of the stack.

Can anyone shed some light on why I'm stock piling my devices memory? (Hopefully something simple I'm completely overlooking) Did I somehow manage to throw ARC out the window?

EDIT: How I call for the image in my other views

- (void)loadBackground
{
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);  
    NSString *documentsPath = [paths objectAtIndex:0];
    NSString *filePath = [documentsPath stringByAppendingPathComponent:@"image.jpeg"];
    UIImage *image = [UIImage imageWithContentsOfFile:filePath];
    [backgroundImageView setImage:image];

}

How navigation between my view controllers is established:

enter image description here

EDIT 2:

What my basic declarations look like:

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
@interface PhotoPickerViewController : UIViewController <UIImagePickerControllerDelegate, UINavigationControllerDelegate>
{
    IBOutlet UIImageView *imageView;
    NSData *jpgData;
    NSString *filePath;
    UIImagePickerController *imagePicker;
    IBOutlet UIBarButtonItem *disableNextButton;
}


@end

If relevant, how I call up my image picker:

- (void)callCameraPicker
{


    if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] == YES)
    {
        NSLog(@"Camera is available and ready");

        imagePicker.sourceType =  UIImagePickerControllerSourceTypeCamera;
        imagePicker.delegate = self;
        imagePicker.allowsEditing = NO;
        imagePicker.cameraCaptureMode = UIImagePickerControllerCameraCaptureModePhoto;

        NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; for (AVCaptureDevice *device in devices) 
        {
            if([[UIScreen mainScreen] respondsToSelector:@selector(scale)] && [[UIScreen mainScreen] scale] == 2.0) 
            {
                imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceFront;


            }
        }


        imagePicker.modalTransitionStyle = UIModalTransitionStyleCoverVertical;

        [self presentModalViewController:imagePicker animated:YES];

    }


    else
    {
        NSLog(@"Camera is not available");
        UIAlertView *cameraAlert = [[UIAlertView alloc] initWithTitle:@"Error" 
                                                              message:@"Your device doesn't seem to have a camera!" 
                                                             delegate:self cancelButtonTitle:@"Dismiss" 
                                                    otherButtonTitles:nil];
        [cameraAlert show];

    }
}

EDIT 3: I logged viewDidUnload, and it was in fact not being called so I'm now calling loadBackground in viewWillAppear and making my backgroundImageView nil in viewDidDisappear. I expected this to help but it made no difference.

- (void)viewWillAppear:(BOOL)animated
{
    [self loadBackground];
}


- (void)viewDidDisappear:(BOOL)animated
{
    NSLog(@"ViewDidDisappear");
    backgroundImageView = nil;
}
like image 745
Mick MacCallum Avatar asked May 12 '12 12:05

Mick MacCallum


2 Answers

The relationship between UIImage and UIImageView is not necessarily intuitive for everyone.

UIImage is a high level representation of your image data - alone, it does nothing in terms of displaying the data.

UIImageView works with UIImage to allow you to display an image.

There is no reason why multiple instances of UIImageView cannot display the same UIImage. This is nice and efficient, because there is only one in-memory representation of the image data, being shared by multiple views.

What you seem to be doing is creating a new UIImage for each one of your views, by loading it from disk. So this is a poor general design in two respects: instantiating what is effectively the same UIImage over and over again, and re-loading the same image data from disk repeatedly.

Your memory problem is really a separate issue, where you are not properly releasing the image data you keep loading into UIImage objects and UIImageViews.

In theory, you should be able to take the very first UIImage you're getting from UIImagePickerController and simply pass that reference around to your views, without reloading from disk.

If you need to be saving and reloading from disk because of higher level functional requirements (e.g. because the image is being changed by the user and you want to keep saving it), you'll need to be sure you are fully tearing down the previous UIView, by removing it from the it's view hierarchy. It is helpful to setup a breakpoint in the dealloc method for the view to confirm it is being removed and dealloced, and make sure you set any iVar references to sub-views (it appears your backgroundImageView is an iVar) are set to nil. If you are not properly tearing down that backgroundImageView, it is continuing to hold a reference to the UIImage you set to it's image property.

like image 194
Rob Reuss Avatar answered Nov 15 '22 19:11

Rob Reuss


There are a couple of things that are curious about the code you posted:

  1. None of your view-callback implementations call super. That’s bad! Make extra sure that you are calling super in viewDidUnload and (if you implemented it) didReceiveMemoryWarning.
  2. Make sure you implement didReceiveMemoryWarning in a meaningful way!
  3. You really should not be re-creating that image over and over again! I assume you are not editing the actual image because you use JPEG compression on it which — even at 100% quality — will deteriorate your image with every save…
  4. Check your implementation of viewDidUnload make sure to set every of your IBOutlets to nil.

ARC is not Pixie Dust™! It just saves you a bit of typing, it does not free you from designing and maintaining your object graphs!

From your question, I see at the very least these graphs that refer to your image:

image 1 <- image-view 1 <- view-controller 1 <- navigation-controller <- key window <- application

image 1 <- image-view 1 <- view 1 <- view-controller 1 <- navigation-controller <- key window <- application

This is repeated for every view-controller with an index shift on the view-controller, view, image view and image. While you have to have separate views, image-views for your view-controllers, I cannot think of a reason why you would want several copies of the same image.

So the first axe on your memory consumption clearly is to no longer create all those copies of the same image data — I’d estimate that this will get you half of the low-hanging memory savings.

The next thing is that ARC can only free the memory consumed by your objects if it is no longer referenced.

Memory wise, views are not exactly lightweight objects and when you build up a deep navigation stack you end up with gobs of them.

So you need to axe any unneeded strong references to those views, as well.

The level, at which this has to happen is the view-controller. The latest time at which this should happen is in the view-controller’s implementation of viewDidUnload.

Why the view-controller?

From what you described, the image itself is only referenced by the UIImageView — this is a bad choice, IMHO, but I digress…
UIViewController is designed to “know”, when its view is needed and when it’s safe to dispose of it — that’s why it implements didReceiveMemoryWarning and viewDidUnload:

If the memory pressure gets to high and the view-controller’s view is not “on screen” the root implementation of didReceiveMemoryWorning will let go of its view and call viewDidUnload upon itself, afterwards.

This is why you must call through to super in your implementations of both of those methods.

In addition, this is why if you have strong IBOutlets that refer to subviews of the view-controller’s view, you must nil them in viewDidUnload or the system cannot reclaim the memory they occupy.

At its heart UIViewController is a big-ass finite state-machine. All of those “something-will/did-whatever” callbacks are used to transition between those states and most of the default implementations do some very important book-keeping to keep all that state in order.

If you are not invoking them in your overrides, you˚ll end up in inconsistent states and bad things — like this out of memory crasher — happen.

like image 37
danyowdee Avatar answered Nov 15 '22 21:11

danyowdee