Objective C UIImagePNGRepresentation memory issue

2020-07-10 05:34发布

问题:

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.

回答1:

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.



回答2:

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);



回答3:

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.



回答4:

You may have better luck with The ImageIO Framework (PDF). It has a bit more control over memory and caching than UIKit.



回答5:

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;
}


回答6:

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.



回答7:

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.