Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is CIContext.createCGImage causing a memory leak?

I have only observed this behaviour on iOS 9; iOS 8 works correctly.

I suspect this might be a bug on the SDK, and I have opened a radar to Apple (22644754), but I find it so strange, that I get the feeling that I might be missing a call or a step to avoid the leak.

What I have observed is that every time CIContext.createCGImage is called, there is a memory usage increase. The tricky part is that the memory increase occurs outside of the app.

If you look at the "Memory Report" from Xcode, the memory increase is visible on the "Other Processes" section.

Basically, what I do to cause the problem is the following (I have simplified the code to strictly the parts necessary to reproduce the leak):

First I create a CIContext backed by a EAGLContext:

let glContext = EAGLContext(API: .OpenGLES2)!
let ciContext = CIContext(EAGLContext: glContext, options: [kCIContextOutputColorSpace : NSNull()])

Then, I render an image using the following:

let input = CIImage(image: UIImage(named: "DummyImage")!)!
ciContext.createCGImage(input, fromRect: input.extent)

DummyImage is just a sample image file. The leak is directly associated to the size of this image, so it is best to use a big one in order to make the problem more noticeable.

As you can see, I'm not using any CIFilters (using them causes the same result), and I'm not capturing the resulting image (Even if I captured it, I couldn't use CGImageRelease as the objects are automatically managed).

If the render code is executed enough times, the memory will grow so much that the running apps will get killed.

One interesting observation is that destroying the CIContext does not make a difference, but destroying the EAGLContext does return the taken memory. That makes me think that the leak is happening on the OpenGL side.

Am I missing anything in my code that can cause the leak? Is there any call I can make to free up the memory taken by the EAGLContext? (Recreating it all the time is not an option as it is a costly operation).


I have created a simple project to reproduce the issue. You can find it at:

https://www.dropbox.com/s/zm19u8rmujv6jet/EAGLContextLeakDemo.zip?dl=0

The steps to reproduce are:

  1. Open and run the attached project on a Device.
  2. Observe the "Memory Report" screen on Xcode. The growth will be seen on the "Other Processes" part of the "Usage Comparison" pie chart.
  3. The app presents three buttons. Each of them will execute the createCGImage command a certain number of times (shown on the button labels).
  4. Tapping on any of the buttons will result on a memory usage increase of the "Other Processes". This might be more noticeable after performing several createCGImage calls.
  5. Tapping on the 100 Renders buttons will show more clearly the effect.
  6. When the memory growth is excessive, the App will crash.
like image 748
flainez Avatar asked Sep 11 '15 09:09

flainez


2 Answers

The way I've solved it was to render the context into a buffer and then write it to file as JPG. I'll have to further see how to optimise this flow on older gen iOS devices, but it seems to work well in comparison to createCGImage. This code is also useful for turning a CIImage into JPEG or bitmap NSData. Full sample code can be seen here:

https://github.com/mmackh/IPDFCameraViewController

static CIContext *ctx = nil;

if (!ctx)
{
    ctx = [CIContext contextWithOptions:@{kCIContextWorkingColorSpace:[NSNull null]}];
}

CGSize bounds = enhancedImage.extent.size;
int bytesPerPixel = 8;
uint rowBytes = bytesPerPixel * bounds.width;
uint totalBytes = rowBytes * bounds.height;
uint8_t *byteBuffer = malloc(totalBytes);

CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

[ctx render:enhancedImage toBitmap:byteBuffer rowBytes:rowBytes bounds:enhancedImage.extent format:kCIFormatRGBA8 colorSpace:colorSpace];

CGContextRef bitmapContext = CGBitmapContextCreate(byteBuffer,bounds.width,bounds.height,bytesPerPixel,rowBytes,colorSpace,kCGImageAlphaNoneSkipLast);

CGImageRef imgRef = CGBitmapContextCreateImage(bitmapContext);

CGColorSpaceRelease(colorSpace);
CGContextRelease(bitmapContext);
free(byteBuffer);

if (imgRef == NULL) { goto release; }


CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:filePath];
CGImageDestinationRef destination = CGImageDestinationCreateWithURL(url, kUTTypeJPEG, 1, NULL);
CGImageDestinationAddImage(destination, imgRef, nil);
CGImageDestinationFinalize(destination);
CFRelease(destination);

success = YES;

goto release;

release :
{
    CFRelease(imgRef);

    if (success)
    {
        //completionHandler(filePath);
    }

    dispatch_resume(_captureQueue);
}
like image 107
mmackh Avatar answered Sep 27 '22 17:09

mmackh


This has been confirmed as a bug on iOS 9. The issue has been resolved starting on iOS 9.1 beta 3.

The code posted in the original question is correct. There are no modifications needed to prevent the leak on versions previous to iOS 9 or versions starting on iOS 9.1 Beta 3.

like image 23
flainez Avatar answered Sep 27 '22 17:09

flainez