15

I've started to use the ARC recently and since then I blame it for every single memory problem. :) Perhaps, you could help me better understand what I'm doing wrong.

My current project is about CoreGraphics a lot - charts drawing, views filled with thumbnails and so on. I believe there would be no problem while using manual memory management, except maybe a few zombies... But as of now, application simply crashes every time I try to either create a lot of thumbnails or redraw a bit more complicated chart.

While profiling with Instruments I can see an awfully high value in resident memory as well as in the dirty one. Heap analysis shows rather alarming irregular grow...

When drawing just a few thumbnails, resident memory grows for about 200 MB. When everything is drawn, memory drops back on almost the same value as before drawing. However, with a lot of thumbnails, a value in resident memory is higher than 400 MB and that obviously crashes the app. I've tried to limit number of thumbnails drawn at the same time (NSOperationQueue and its maxConcurrentOperationCount), but as releasing so much memory seems to take a bit more time, it didn't really solve the issue.

Right now my app basically doesn't work as the real data works with a lot of complicated charts = lot of thumbnails.

Every thumbnail is created with this code I got from around here: (category on UIImage)

+ (void)beginImageContextWithSize:(CGSize)size
{
    if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
        if ([[UIScreen mainScreen] scale] == 2.0) {
            UIGraphicsBeginImageContextWithOptions(size, YES, 2.0);
        } else {
            UIGraphicsBeginImageContext(size);
        }
    } else {
        UIGraphicsBeginImageContext(size);
    }
}

+ (void)endImageContext
{
    UIGraphicsEndImageContext();
}

+ (UIImage*)imageFromView:(UIView*)view
{
    [self beginImageContextWithSize:[view bounds].size];
    BOOL hidden = [view isHidden];
    [view setHidden:NO];
    [[view layer] renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    [self endImageContext];
    [view setHidden:hidden];
    return image;
}

+ (UIImage*)imageFromView:(UIView*)view scaledToSize:(CGSize)newSize
{
    UIImage *image = [self imageFromView:view];
    if ([view bounds].size.width != newSize.width ||
        [view bounds].size.height != newSize.height) {
        image = [self imageWithImage:image scaledToSize:newSize];
    }
    return image;
}

+ (UIImage*)imageWithImage:(UIImage*)image scaledToSize:(CGSize)newSize
{
    [self beginImageContextWithSize:newSize];
    [image drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    [self endImageContext];
    return newImage;
}

Is there some other way which wouldn't eat so much memory or is something really wrong with the code when using ARC?

The other place where memory warning + crash is happening is when there is too much redrawing of any view. It doesn't need to be quick, just many times. Memory stacks up until it crashes and I'm not able to find anything really responsible for it. (I can see a growing resident/dirty memory in VM Tracker and a heap growth in Allocations instrument)

My question basically is: how to find why it is even happening? My understanding is when there is no owner for given object, it's released ASAP. My inspection of code suggests a lot of objects are not released at all even though I don't see any reason for it to happen. I don't know about any retain cycles...

I've read through the Transitioning to ARC Release Notes, bbum's article about heap analysis and probably a dozen of others. Differs somehow heap analysis with and without ARC? I can't seem to do anything useful with its output.

Thank you for any ideas.

UPDATE: (to not force everybody read all the comments and to hold my promise)

By carefully getting through my code and adding @autoreleasepool, where it had any sense, memory consumption got lowered. The biggest problem was calling UIGraphicsBeginImageContext from background thread. After fixing it (see @Tammo Freese's answer for details) deallocation occurred soon enough to not crash an app.

My second crash (caused by many redrawing of the same chart), was completely solved by adding CGContextFlush(context) at the end of my drawing method. Shame on me.


A small warning for anyone trying to do something similar: use OpenGL. CoreGraphics is not quick enough for animating big drawings, especially not on an iPad 3. (first one with retina)

xius
  • 596
  • 1
  • 6
  • 20
  • The compiler does not automatically manage the lifetimes of Core Foundation objects; you must call CFRetain and CFRelease (or the corresponding type-specific variants) as dictated by the Core Foundation memory management rules (see Memory Management Programming Guide for Core Foundation). See http://developer.apple.com/library/ios/#releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html Also an excellent post about ARC, Toll Free bridging and Core Graphics can be found here: http://stackoverflow.com/a/7800359/312312 – Lefteris Jul 24 '12 at 13:23
  • @Lefteris thanks, I've read the Apple's article and watched the videos mentioned on the answer you link, but when I'm drawing I'm not really retaining anything, just plain `CGContextAddLineToPoint`, `CGContextAddRect`,... which return void. Should I release context I get from `CGContextRef context = UIGraphicsGetCurrentContext();`? I'll look into it, though. – xius Jul 24 '12 at 15:50

3 Answers3

18

To answer your question: Identifying problems with memory warnings and crashes with ARC basically works like before with manual retain-release (MRR). ARC uses retain, release and autorelease just like MRR, it only inserts the calls for you, and has some optimizations in place that should even lower the memory consumption in some cases.

Regarding your problem:

In the screenshot of Instruments you posted, there are allocation spikes visible. In most cases I encountered so far, these spikes were caused by autoreleased objects hanging around too long.

  1. You mentioned that you use NSOperationQueue. If you override -[NSOperationQueue main], make sure that you wrap the whole content of the method in @autoreleasepool { ... }. An autorelease pool may already be in place, but it is not guaranteed (and even if there is one, it may be around for longer than you think).

  2. If 1. has not helped and you have a loop that processes the images, wrap the inner part of the loop in @autoreleasepool { ... } so that temporary objects are cleaned up immediately.

  3. You mentioned that you use NSOperationQueue. Since iOS 4, drawing to a graphics context in UIKit is thread-safe, but if the documentation is right, UIGraphicsBeginImageContext should still only be called on the main thread! Update: The docs now state that since iOS 4, the function can be called from any thread, to the following is actually unnecessary! To be on the safe side, create the context with CGBitmapContextCreate and retrieve the image with CGBitmapContextCreateImage. Something along these lines:

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, colorSpace, kCGImageAlphaPremultipliedLast);
    CGColorSpaceRelease(colorSpace);
    
    // draw to the context here
    
    CGImageRef newCGImage = CGBitmapContextCreateImage(context);
    CGContextRelease(context);
    UIImage *result = [UIImage imageWithCGImage:newCGImage scale:scale orientation: UIImageOrientationUp];
    CGImageRelease(newCGImage);
    
    return result;
    
Tammo Freese
  • 10,514
  • 2
  • 35
  • 44
  • Thanks! Seems as possible solution to memory issues, but CGBitmapContextCreate returns always nil and I haven't found out why yet...? @autoreleasepool is all around my code. :) (all where you mentioned) – xius Jul 30 '12 at 18:49
  • Have the `@autoreleasepool`s helped, or do you still see the memory issues? As to `CGBitmapContextCreate`, it returns `NULL` if it could not create a context, which can be caused by wrong arguments. Try whether `CGBitmapContextCreate(NULL, 640, 480, 8, 0, colorSpace, kCGImageAlphaPremultipliedLast)` works for you, it should return non-`NULL`. – Tammo Freese Jul 30 '12 at 19:52
  • When `CGBitmapContextCreate` returns `NULL`, check that you are using valid parameters. On OS X, check the console for messages about bad parameters to `CGBitmapContextCreate` – nielsbot Jul 30 '12 at 20:48
  • also, for missing `@autoreleasepool`, you will see a "no NSAutoreleasePool in place, just leaking...` in your program output. Look for that to make sure. – nielsbot Jul 30 '12 at 20:49
  • @TammoFreese Wow! Much much less memory is used, seems like drawn view is released almost immediately! I don't have access to a iDevice now, but seems very promising. I check it in next few hours. – xius Jul 30 '12 at 22:01
  • @TammoFreese Also, context was nil because I was trying to create it with the width = 0 and height = 0, thanks for helping with this as well. :) – xius Jul 30 '12 at 22:02
  • @nielsbot I have got neither message about the bad parameters, nor 'no NSAutoreleasePool in place, just leaking...', should I have turned on something in Xcode, perhaps? At least first message would certainly be right. – xius Jul 30 '12 at 22:04
  • You don't have to do anything, AFAIK, however your call is incorrect. bytes per row cannot be `0` afaik. It should be (bits per pixel / 8) * width. With 4 comp color space, 8 bpc, it's `width * 4`. Also, add byte order info to bitmapInfo: `CGBitmapContextCreate(NULL, width, height, 8, 4 * width, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast)` – nielsbot Jul 30 '12 at 22:52
  • @nielsbot 1) `bytesPerRow` can be `0`, then the correct value is calculated. It's missing in the docs, but often used in Apple's examples. But you are right, als long as it is not in the docs, better calculate `bytesPerRow`. 2) An autorelease pool can be in place, but live too long. Example GCD: There is always an autorelease pool in place, but there is no guarantee when it will be drained. 3) `kCGBitmapByteOrderDefault` can be added for clarity, but it has no other effect – if you don't add it, the default byte order will be used anyway, since the constant `kCGBitmapByteOrderDefault` is `0`. – Tammo Freese Jul 31 '12 at 09:05
  • @xius Could you tell us which of the fixes has been the one reducing the memory consumption? – Tammo Freese Jul 31 '12 at 09:07
  • @TammoFreese Not 100% sure, as there is only first gen iPad available now and the app isn't optimized for it... but memory doesn't stack up, it's correctly released - seems as you're the winner! :) I think getting rid of `UIGraphicsBeginImageContext` and adding `@autoreleasepool` to `[NSOperationQueue main]` where critical. (I've fixed really silly mistake in drawing charts itself which probably also greatly helped :)) I'll update my question later with all the changes. Thank you! :) – xius Jul 31 '12 at 10:34
  • @TammoFreese good to know--although I'll probably keep specifying `kCGBitmapByteOrderDefault` for clarity – nielsbot Jul 31 '12 at 18:26
  • Since iOS 4, ``UIGraphicsBeginImageContext`` can be called from any thread. – amb Jun 18 '14 at 08:28
  • Thanks, I think that information was missing in the docs back then. I'll update my answer. – Tammo Freese Jun 18 '14 at 11:53
  • WOW! Step 2 solved my problem. I have been stuck on this for weeks! – JCutting8 Dec 13 '15 at 03:54
  • @JCutting8 Glad I could help! – Tammo Freese Apr 12 '16 at 20:06
2

So, nothing you are doing relative to memory management (there is none!) looks improper. However, you mention using NSOperationQueue. Those UIGraphics... calls are marked as not thread safe, but others have stated they are as of iOS 4 (I cannot find a definitive answer to this, but recall that this is true.

In any case, you should not be calling these class methods from multiple threads. You could create a serial dispatch queue and feed all the work through that to insure single threaded usage.

What's missing here of course is what you do with the images after using them. Its possible you are retaining them in some way that is not obvious. Here are some tricks:

  • in any of your classes that use lots of images, add a dealloc() method that just logs its name and some identifier.

  • you can try to add a dealloc to UIImage to do the same.

  • try to drive your app using the simplest possible setup - fewest images etc - so you can verify that in fact that the images and their owners are getting dealloc'd.

  • when you want to make sure something is released, set the ivar or property to nil

I converted a 100 file project to ARC last summer and it worked perfectly out of the box. I have converted several open source projects to ARC as well with only one problem when I improperly used bridging. The technology is rock solid.

David H
  • 40,852
  • 12
  • 92
  • 138
  • 1
    Thanks for some ideas! I'll try it tomorrow. I can also confirm a problem is not in ARC - I've rewritten the whole app to manually manage memory and guess what... everything is exactly the same. (at least from a problem discussed here point of view) – xius Jul 24 '12 at 15:41
2

This is not an answer to your question but I was trying to solve similar problems long before ARC was introduced. Recently I was working on an application that was caching images in memory and releasing them all after receiving memory warning. This worked fine as long as I was using the application at a normal speed (no crazy tapping). But when I started to generate a lot of events and many images started to load, the application did not manage to get the memory warning and it was crashing.

I once wrote a test application that was creating many autoreleased objects after tapping a button. I was able to tap faster (and create objects) than the OS managed to release the memory. The memory was slowly increasing so after a significant time or simply using bigger objects I would surely crash the application and cause device to reboot (looks really effective ;)). I checked that using Instruments which unfortunately affected the test and made everything slower but I suppose this is true also when not using Instruments.

On the other occasion I was working on a bigger project that is quite complex and has a lot of UI created from code. It also has a lot of string processing and nobody cared to use release - there were few thousands of autorelease calls when I checked last time. So after 5 minutes of slightly extensive usage of this application, it was crashing and rebooting the device.

If I'm correct then the OS/logic that is responsible for actually deallocating memory is not fast enough or has not high enough priority to save an application from crashing when a lot of memory operations are performed. I never confirmed these suspicions and I don't know how to solve this problem other than simply reducing allocated memory.

Mateusz
  • 314
  • 1
  • 5
  • I've also managed to reset a device by tapping frantically around! :) I've managed to lower memory requirements a little bit to let me run it slowly. Not enough for real use yet - sign of bad design, I guess. My bad I've never realized I actually could use all available memory. If only I'd read an answer like yours here before. :) Thanks. – xius Jul 25 '12 at 13:57