I have an ARC based app which loads about 2,000 fairly large (1-4MB) Base64 encoded images from a webservice. It converts the Base64 decoded strings to .png
image files and saves them to the disk. This is all done in a loop where i shouldn't have any lingering references.
I profiled my app and found out that UIImagePNGRepresentation was hogging around 50% of available memory.
The way i see it, UIImagePNGRepresentation is caching the images it creates. One way to fix this would be to flush that cache. Any ideas how one might do that?
Another solution would be to use something other than UIImagePNGRepresentation?
I already tried out this with no luck: Memory issue in using UIImagePNGRepresentation. Not to mention that i can't really use the solution provided there 'cause it would make my app too slow.
This is the method i call from my loop. UIImage is the image converted from Base64:
+ (void)saveImage:(UIImage*)image:(NSString*)imageName:(NSString*)directory {
NSData *imageData = UIImagePNGRepresentation(image); //convert image into .png format.
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); //create an array and store result of our search for the documents directory in it
NSString *documentsDirectory = [paths objectAtIndex:0]; //create NSString object, that holds our exact path to the documents directory
NSString *pathToFolder = [documentsDirectory stringByAppendingPathComponent:directory];
if (![fileManager fileExistsAtPath:pathToFolder]) {
if(![fileManager createDirectoryAtPath:pathToFolder withIntermediateDirectories:YES attributes:nil error:NULL]) {
// Error handling removed for brevity
}
}
NSString *fullPath = [pathToFolder stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.png", imageName]]; //add our image to the path
[fileManager createFileAtPath:fullPath contents:imageData attributes:nil]; //finally save the path (image)
// clear memory (this did nothing to improve memory management)
imageData = nil;
fileManager = nil;
}
EDIT:
Image dimensions vary roughly from 1000*800 to 3000*2000.
You could wrap the method body by a autorelease pool
+ (void)saveImage:(UIImage*)image:(NSString*)imageName:(NSString*)directory {
@autoreleasepool
{
NSData *imageData = UIImagePNGRepresentation(image); //convert image into .png format.
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); //create an array and store result of our search for the documents directory in it
NSString *documentsDirectory = [paths objectAtIndex:0]; //create NSString object, that holds our exact path to the documents directory
NSString *pathToFolder = [documentsDirectory stringByAppendingPathComponent:directory];
if (![fileManager fileExistsAtPath:pathToFolder]) {
if(![fileManager createDirectoryAtPath:pathToFolder withIntermediateDirectories:YES attributes:nil error:NULL]) {
// Error handling removed for brevity
}
}
NSString *fullPath = [pathToFolder stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.png", imageName]]; //add our image to the path
[fileManager createFileAtPath:fullPath contents:imageData attributes:nil]; //finally save the path (image)
}
}
But actually it could be helpful, if you provide us with some more numbers:
What dimensions do the images have. This is important, as image data is stored in memory in raw pixels. iE a image 2000px width * 2000px height * 4 Bytes (RGBA) ~ 15MB
. Now imagine, that the converting algorithm will have to store informations for every pixel or at least some area. Huge numbers are to be expected.
I had the same issue with UIImagePNGRepresentation and ARC.
My project is generating tiles and the allocated memory by UIImagePNGRepresentation was just not removed even when the UIImagePNGRepresentation call is part of a @autoreleasepool.
I didn't had the luck that the issue is disappeared by adding a few more @autoreleasepool's like it did for JHollanti.
My solution is based on EricS idea, using the ImageIO Framework to save the png file:
-(void)saveImage:(CGImageRef)image directory:(NSString*)directory filename:(NSString*)filename {
@autoreleasepool {
CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/%@", directory, filename]];
CGImageDestinationRef destination = CGImageDestinationCreateWithURL(url, kUTTypePNG, 1, NULL);
CGImageDestinationAddImage(destination, image, nil);
if (!CGImageDestinationFinalize(destination))
NSLog(@"ERROR saving: %@", url);
CFRelease(destination);
CGImageRelease(image);
}
}
Most important is to release the image afterwards: CGImageRelease(image);
Is it necessary to convert the data here, maybe convert it on the load from disk?
The NSData object maybe make it NSMutableData this way the memory for it is allocated once and grows as needed.
You may have better luck with The ImageIO Framework (PDF). It has a bit more control over memory and caching than UIKit.
Try this:
+ (void)saveImage:(UIImage*)image:(NSString*)imageName:(NSString*)directory {
NSData *imageData = UIImagePNGRepresentation(image); //convert image into .png format.
CFDataRef imageDataRef = (__bridge_retained CFDataRef) imageData; // ARC fix
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); //create an array and store result of our search for the documents directory in it
NSString *documentsDirectory = [paths objectAtIndex:0]; //create NSString object, that holds our exact path to the documents directory
NSString *pathToFolder = [documentsDirectory stringByAppendingPathComponent:directory];
if (![fileManager fileExistsAtPath:pathToFolder]) {
if(![fileManager createDirectoryAtPath:pathToFolder withIntermediateDirectories:YES attributes:nil error:NULL]) {
// Error handling removed for brevity
}
}
NSString *fullPath = [pathToFolder stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.png", imageName]]; //add our image to the path
[fileManager createFileAtPath:fullPath contents:imageData attributes:nil]; //finally save the path (image)
// clear memory (this did nothing to improve memory management)
imageData = nil;
CFRelease(imageDataRef); // ARC fix
fileManager = nil;
}
Just one of the possible ways is to use github's libs that downloads and caches UIImage/NSData from Internet. It may by SDWebImage(https://github.com/rs/SDWebImage) or APSmartStorage (https://github.com/Alterplay/APSmartStorage). Latest gets an image from Internet and stores it smartly on disk and in memory.
I solve this issue by sending a 4 channels image (RGBA or RGBX) instead of a 3 channels image (RGB).
You can check if there's any chance to change parameters of your image.
When you convert Base64 to UIImage, try to use kCGImageAlphaNoneSkipLast
instead of kCGImageAlphaNone
.