11

I have a large UIScrollView into which I'm placing 3-4 rather large (320x1500 pixels or so) UIImageView image tiles. I'm adding these UIImageViews to the scroll view inside of my NIB files. I have one outlet on my controller, and that is to the UIScrollView. I'm using a property (nonatomic, retain) for this, and sythesizing it.

My question is this: When I observe this in Memory Monitor, I can see that the memory used goes up quite a bit when the view with all these images is loaded (as expected). But when I leave the view, it and its controller are dealloc'd, but do not seem to give up anywhere near the memory they had taken up. When I cut one of these views (there are several in my app) down to just 1-3 images that were 320x460 and left everything else the same, it recaptures the memory just fine.

Is there some issue with using images this large? Am I doing something wrong in this code (pasted below)?

This is a snippet from the viewController that is causing problems.

- (CGFloat)findHeight 
{
    UIImageView *imageView = nil;
    NSArray *subviews = [self.scrollView subviews];

    CGFloat maxYLoc = 0;
    for (imageView in subviews)
    {
            if ([imageView isKindOfClass:[UIImageView class]])
            {
                    CGRect frame = imageView.frame;

                    if ((frame.origin.y + frame.size.height) > maxYLoc) {
                            maxYLoc  = frame.origin.y;
                            maxYLoc += frame.size.height;
                    }
            }
    }
    return maxYLoc;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.scrollView setContentSize:CGSizeMake(320, [self findHeight])];

    [self.scrollView setCanCancelContentTouches:NO];
    self.scrollView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
    self.scrollView.clipsToBounds = YES;                
    self.scrollView.scrollEnabled = YES;

    self.scrollView.pagingEnabled = NO;
}

- (void)dealloc {
    NSLog(@"DAY Controller Dealloc'd");
    self.scrollView = nil;
    [super dealloc];
}

UPDATE: I've noticed another weird phenomenon. If I don't use the scroll on the view, it seems to be hanging on to the memory. But if I scroll around a bunch and ensure that all of the UIImageViews became visible at one point, it will free up and regain most of the memory it lost.

UPDATE2: The reason I'm asking this is my app is actually crashing due to low memory. I wouldn't mind if it were just caching and using up extra memory, but it doesn't seem to ever release it - even in didReceiveMmoryWarning conditions

Bdebeez
  • 3,542
  • 3
  • 31
  • 32

7 Answers7

15

I've solved the mystery - and I'm pretty sure this is a bug on Apple's side.

As Kendall suggested (thanks!), the problem lies in how InterfaceBuilder loads images from the NIB file. When you initFromNib, all UIImageViews will init with a UIImage using the imageNamed: method of UIImage. This call uses caching for the image. Normally, this is what you want. However, with very large images and additionally ones that scroll far off of the visible area, it does not seem to be obeying memory warnings and dumping this cache. This is what I believe to be a bug on Apple's side (please comment if you agree/disagree - I'd like to submit this if others agree). As I said above, the memory used by these images does seem to be released if a user scrolls around enough to make it all visible.

The workaround that I've found (also Kendall's suggestion) is to leave the image name blank in the NIB file. So you lay out your UIImageView elements as normal, but don't select an image. Then in your viewDidLoad code, you go in and load an image using imageWithContentsOfFile: instead. This method does NOT cache the image, and therefore does not cause any memory issues with retaining large images.

Of course, imageNamed: is a lot easier to use, because it defaults to anything in the bundle, rather than having to find the path. However, you can get the path to the bundle with the following:

NSString *fullpath = [[[NSBundle mainBundle] bundlePath];

Putting that all together, here's what that looks like in code:

NSString *fullpath = [[[NSBundle mainBundle] bundlePath] stringByAppendingString:[NSString stringWithFormat:@"/%@-%d.png", self.nibName, imageView.tag]];
UIImage *loadImage = [UIImage imageWithContentsOfFile:fullpath];
imageView.image = loadImage;

So adding that to my code above, the full function looks like this:

- (CGFloat)findHeight 
{
    UIImageView *imageView = nil;
    NSArray *subviews = [self.scrollView subviews];

    CGFloat maxYLoc = 0;
    for (imageView in subviews)
    {
        if ([imageView isKindOfClass:[UIImageView class]])
        {
            CGRect frame = imageView.frame;

            if ((frame.origin.y + frame.size.height) > maxYLoc) {
                maxYLoc  = frame.origin.y;
                maxYLoc += frame.size.height;
            }

            NSString *fullpath = [[[NSBundle mainBundle] bundlePath] stringByAppendingString:[NSString stringWithFormat:@"/%@-%d.png", self.nibName, imageView.tag]];
            NSLog(fullpath);


            UIImage *loadImage = [UIImage imageWithContentsOfFile:fullpath];
            imageView.image = loadImage;
        }
    }
    return maxYLoc;
}
Bdebeez
  • 3,542
  • 3
  • 31
  • 32
  • kinda late but did you submit the bug to apple? Cause I agree on this it is a bug on there side. It never gets deleted. The same issue is happening for me when using imageWithData. – Rana Tallal Jul 31 '15 at 09:12
4

Separate from your imageNamed caching issues, the answer to your question

I have a large UIScrollView into which I'm placing 3-4 rather large (320x1500 pixels or so) UIImageView image tiles. [...] Is there some issue with using images this large?

Is in the header of the UIImage docs:

You should avoid creating UIImage objects that are greater than 1024 x 1024 in size.
Besides the large amount of memory such an image would consume, you may run into 
problems when using the image as a texture in OpenGL ES or when drawing the image 
to a view or layer.

Also, Apple claims to have fixed the imageNamed cache-flushing problem in 3.0 and beyond, although I've not tested this extensively, myself.

Olie
  • 24,597
  • 18
  • 99
  • 131
  • I'm currently running into some memory problems likely caused by imageNamed caching the images when I really do not need them to be cached, nor do I want them to be cached. It's like telling somebody to go do x to 100 y's, and instead of leaving each y where it is when they're done, they're carrying them all with them no matter what until the weight of all the y's crush them and they are killed. – Sneakyness Dec 02 '09 at 16:53
  • So no, it is not fixed as of 3.1.2. – Sneakyness Dec 02 '09 at 16:54
  • That comment about restricting image size seems a little odd as the iPad natively deals fine with images that size and bigger, right? So, what do you suppose they're using? – Josh Nov 19 '10 at 05:04
  • @Josh: UIImage docs state that an image may have problems displaying larger than 1024x1024. The limit may apply more on older devices with less RAM. This is speculation on my part, but seems consistent with observations. – Olie Nov 30 '10 at 18:38
  • @Sneakyness: are you sure you don't have a spare retain, somewhere? I've got apps dealing with 100s (sometimes > 1000!) `imageNamed:` images and, while the system does cache them, it also releases the unused ones when more memory is needed. – Olie Nov 30 '10 at 18:39
  • This was on the original iPhone and the iPhone 3G, long before the time of the 3GS and 4. The entire platform was simply far too slow to support concurrently loading full-screen pngs >12-16 times a second while loading and playing sound. – Sneakyness Dec 07 '10 at 07:19
4

It could be the system caching references to your images in memory, assuming you have just drug in the images from the media browser in IB the code underneath is probably using the UIImage imageNamed: method...

You could try loading all the images with imageWithContentsOfFile: , or imageWithData: and see if it behaves the same (dragging in unfilled UIImageViews in IB and setting contents in viewDidLoad:).

Read the UIImage class reference if you'd like a little more detail, it also describes which methods are cached.

If it's the cache it's probably OK though as the system would free it if needed (did you also try hitting the Simulate Memory Warning in the simulator?)

Adrian P
  • 6,479
  • 4
  • 38
  • 55
Kendall Helmstetter Gelner
  • 74,769
  • 26
  • 128
  • 150
1

Completing the answer of Bdebeez.

One nice idea is to override the imageNamed: calling the imageWithContentsOfFile:.

Here is the idea of the code:

@implementation UIImage(imageNamed_Hack)

+ (UIImage *)imageNamed:(NSString *)name {

    return [UIImage imageWithContentsOfFile:[NSString stringWithFormat:@"%@/%@", [[NSBundle mainBundle] bundlePath], name ] ];
}

@end

Note: With this override you will not have any cache loading UIImages, if you need this, you will have to implement your own cache.

occulus
  • 16,959
  • 6
  • 53
  • 76
  • 2
    That seems like an extraordinarily bad idea. – Rog Jul 23 '14 at 11:21
  • Overriding default method names with categories may(and probably will) lead to undefined behaviour. If you really wan't to override this method you should use method swizzling. However best approach is just to write your own method name imageNamedNoCache and this if you need. – hris.to Mar 27 '15 at 14:55
0

There is a known problem - a memory leak in imageName. I found a really useful solution for it - creating image cash in application delegate, this way optimizing the performance and memory usage in my application. See this blog post

Nava Carmon
  • 4,523
  • 3
  • 40
  • 74
0
- (void)dealloc {
    NSLog(@"DAY Controller Dealloc'd");
    [self.scrollView release];
    [super dealloc];
}

give that a shot, your @property() definition is requesting it to be retained, but you weren't explicitly releasing the object

seanalltogether
  • 3,542
  • 3
  • 26
  • 24
  • I've actually tried this. Doing it the way I did guarantees that the pointer becomes nil after the release. That's because it calls the generated setter, which first retains nil (no effect), then releases scrollView, then sets scrollView to nil. (I also ran it just now to doublecheck :) ) – Bdebeez Nov 14 '08 at 06:37
0

From what I learned on its memory management behavior is that views won't get dealloc unless low in memory. Try an official demo like SQLBooks: 1. Run with Leaks Monitor 2. Run through every views it has. 3. Go back to the root view. 4. You will notice the memory usage level is still the same. As Kendall said it may be cached?

I think you shouldn't pay attention on this memory usage pattern -- because when the new images are pointed to the UIScrollView, the old image objects will be released and memory will be freed for new images anyway.

leonho
  • 3,563
  • 2
  • 21
  • 16