Laurent Crivello
Laurent Crivello

Reputation: 3931

Huge memory usage despite ARC


I have the following function that opens an image, scales it and saves it to another file.

-(void)writeFileToIcon:(NSString *)fullPath :(NSString *)finalPath :(NSSize)outputSize
{
    NSData *dataToWrite;
    NSBitmapImageRep *rep;

    rep = [NSBitmapImageRep imageRepWithData:[[self scaleImage:[[NSImage alloc]initWithContentsOfFile:fullPath] toSize:outputSize] TIFFRepresentation]];
    dataToWrite = [rep representationUsingType:NSPNGFileType properties:nil];
    [dataToWrite writeToFile:finalPath atomically:YES];
}

- (NSImage *)scaleImage:(NSImage *)image toSize:(NSSize)targetSize
{
    if ([image isValid])
    {
        NSSize imageSize = [image size];
        float width  = imageSize.width;
        float height = imageSize.height;
        float targetWidth  = targetSize.width;
        float targetHeight = targetSize.height;
        float scaleFactor  = 0.0;
        float scaledWidth  = targetWidth;
        float scaledHeight = targetHeight;

        NSPoint thumbnailPoint = NSZeroPoint;

        if (!NSEqualSizes(imageSize, targetSize))
        {
            float widthFactor  = targetWidth / width;
            float heightFactor = targetHeight / height;

            if (widthFactor < heightFactor)
            {
                scaleFactor = widthFactor;
            }
            else
            {
                scaleFactor = heightFactor;
            }

            scaledWidth  = width  * scaleFactor;
            scaledHeight = height * scaleFactor;

            if (widthFactor < heightFactor)
            {
                thumbnailPoint.y = (targetHeight - scaledHeight) * 0.5;
            }

            else if (widthFactor > heightFactor)
            {
                thumbnailPoint.x = (targetWidth - scaledWidth) * 0.5;
            }

            NSImage *newImage = [[NSImage alloc] initWithSize:NSMakeSize(scaledWidth, scaledHeight)];

            [newImage lockFocus];

            NSRect thumbnailRect;
            thumbnailRect.origin = NSZeroPoint;
            thumbnailRect.size.width = scaledWidth;
            thumbnailRect.size.height = scaledHeight;

            [image drawInRect:thumbnailRect
                     fromRect:NSZeroRect
                    operation:NSCompositeSourceOver
                     fraction:1.0];

            [newImage unlockFocus];
            return newImage;
        }
        return nil;
    }
    return nil;
}

However each time this function is called, the memory usage is getting higher (up to 5 GB for 1000 calls).
The issue is the drawRect function which seems to take a lot of memory (according to the analyser) but does not release it.
How can I "ask" ARC to release it ?

Thanks.

Upvotes: 3

Views: 235

Answers (3)

CRD
CRD

Reputation: 53010

My guess is your issue is related to caching in the image classes, but that could be wrong. What does appear to improve matters:

-(void)writeFileToIcon:(NSString *)fullPath :(NSString *)finalPath :(NSSize)outputSize
{
    // wrap in autorelease pool to localise any use of this by the image classes
    @autoreleasepool
    {
        NSImage *dstImage = [self scaleImageFile:finalPath toSize:outputSize];
        NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithData:[dstImage TIFFRepresentation]];
        NSData *dataToWrite = [rep representationUsingType:NSPNGFileType properties:nil];
        [dataToWrite writeToFile:finalPath atomically:YES];
    }
}

- (NSImage *)scaleImageFile:(NSString *)fullPath toSize:(NSSize)targetSize
{
    NSImageRep *srcImageRep = [NSImageRep imageRepWithContentsOfFile:fullPath];
    if (srcImageRep == nil)
        return nil;

    NSSize imageSize = NSMakeSize(srcImageRep.pixelsWide, srcImageRep.pixelsHigh);
    NSSize scaledSize;
    NSPoint thumbnailPoint = NSZeroPoint;
    NSRect thumbnailRect;

    if (!NSEqualSizes(imageSize, targetSize))
    {
        // your existing scale calculation
        ...

        scaledSize = NSMakeSize(scaledWidth, scaledHeight);
    }
    else
        scaledSize = imageSize;

    srcImageRep.size = scaledSize;

    NSImage *newImage = [[NSImage alloc] initWithSize:scaledSize];

    [newImage lockFocus];

    thumbnailRect.origin = NSZeroPoint;
    thumbnailRect.size = scaledSize;

    [srcImageRep drawInRect:thumbnailRect];

    [newImage unlockFocus];
    return newImage;
}

This uses NSImageRep which appears in this case to reduce memory footprint. On a sample run using full screen desktop images scaled to 32x32 the above hovered around 16Mb while the original NSImage based version steadily grew to 32Mb. YMMV of course.

HTH

Upvotes: 1

George
George

Reputation: 1255

One may need to look at the whole code to find the problem. One idea follows, though: under ARC you cannot call "release" on objects, but if you set the pointer to the object to "nil", the object will be released (unless other strong references to that object exist somewhere).

I suggest you track your code and make sure you don't hold objects you don't need anymore. If your code is well encapsulated and structured, this shouldn't happen.

If your code is well designed, though, there is the chance that this amount of memory is actually needed (unlikely, but don't know without more details). If this would be the case, then let the system manage the memory, it will release the objects when it is appropriate. This, and try to make optimizations somewhere if memory usage is a concern.

Off-topic: these long nested if's with multiple return points within the method are not a very good idea; I suggest you reestructure your code slightly. If you write clearer code, you'll have more control over it, and you will find solutions to problems faster.

Upvotes: 2

Steven Fisher
Steven Fisher

Reputation: 44886

Are you calling this from a loop or without returning to the main event loop? Adding an explicit @autoreleasepool might help.

-(void)writeFileToIcon:(NSString *)fullPath :(NSString *)finalPath :(NSSize)outputSize
{
    @autoreleasepool {
        NSBitmapImageRep *rep = [NSBitmapImageRep imageRepWithData:[[self scaleImage:[[NSImage alloc]initWithContentsOfFile:fullPath] toSize:outputSize] TIFFRepresentation]];
        NSData *dataToWrite = [rep representationUsingType:NSPNGFileType properties:nil];
        [dataToWrite writeToFile:finalPath atomically:YES];
    }
}

Theoretically, this isn't necessary as code compiled with ARC short circuits the autoreleasepool in some circumstances. However, you may be defeating that optimization here somehow.

Note that it's generally better to do this in the place where the memory allocation becomes the problem logically. So your for loop where you call this method would be a better place for the @autoreleasepool.

Upvotes: 1

Related Questions