Memory issue when using large UIImage array causes crashing (swift)

12,546

Solution 1

Let me start with easy answer: You should not implement stuff that has been experienced by thousands of people by yourself. There are some great libraries that take care of that problem by itself, by implementing disk cache, memory cache, buffers.. Basically everything you will ever need, and more.

Two libraries that I can recommend to you are following:

Both of them are great so it is really matter of preference (I like Haneke better), but they allow you to download images on different threads, be it from Web or from your bundle, or from file system. They also have extensions for UIImageView which allows you to use 1-line function to load all images easily and while you load those images, they care about loading.

Cache

For your specific problem you can use cache that uses those methods to deal with the problem, like this (from documentation):

[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];

Now when you have it in this cache, you can retrieve it easily

SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
[imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image) {
    // image is not nil if image was found
}];

All the memory handling and balancing is done by library itself, so you don't have to worry about anything. You can optionally combine it with resizing methods to store smaller images if those are huge, but that is up to you.

Hope it helps!

Solution 2

I've run into low-memory problems myself in my own apps which have to work with a number of high resolution UIImage objects.

The solution is to save thumbnails of your images (which take a lot less memory) in your imageArray and then display those. If the user really needs to see the full resolution image, you could allow them to click through on the image and then reload & display the full size UIImage from the camera roll.

Here's some code that allows you to create thumbnails:

// image here is your original image
let size = CGSizeApplyAffineTransform(image.size, CGAffineTransformMakeScale(0.5, 0.5))
let hasAlpha = false
let scale: CGFloat = 0.0 // Automatically use scale factor of main screen

UIGraphicsBeginImageContextWithOptions(size, !hasAlpha, scale)
image.drawInRect(CGRect(origin: CGPointZero, size: size))

let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
imageArray.append(scaledImage)

And more information about these techniques can be found in this NSHipster article.

Swift 4 -

// image here is your original image
let size = image.size.applying(CGAffineTransform(scaleX: 0.5, y: 0.5))
let hasAlpha = false
let scale: CGFloat = 0.0 // Automatically use scale factor of main screen

UIGraphicsBeginImageContextWithOptions(size, !hasAlpha, scale)
image.draw(in: CGRect(origin: .zero, size: size))

let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

Solution 3

The best practice is to keep the imageArray short. The array should only be used to cache the images that are in the current scroll range (and the ones that are about to show for better user experience). You should keep the rest in CoreData and load them dynamically during scroll. Otherwise, the app will eventually crash even with the use of thumbnail.

Solution 4

When you receive the memory warning from your view controller you could delete the photos that you are not displaying from your array and save them as a file, then load them again when they are required and so on. Or Simply detecting when they disappear with collectionView:didEndDisplayingCell:forItemAtIndexPath

Save them in an array like this:

var cachedImages = [(section: Int, row: Int, imagePath: String)]()

Using:

func saveImage(indexPath: NSIndexPath, image: UIImage) {
    let imageData = UIImagePNGRepresentation(image)
    let documents = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
    let imagePath = (documents as NSString).stringByAppendingPathComponent("\(indexPath.section)-\(indexPath.row)-cached.png")

    if (imageData?.writeToFile(imagePath, atomically: true) == true) {
        print("saved!")
        cachedImages.append((indexPath.section, indexPath.row, imagePath))
    }
    else {
        print("not saved!")
    }
}

And get them back with:

func getImage(indexPath indexPath: NSIndexPath) -> UIImage? {
    let filteredCachedImages = cachedImages.filter({ $0.section == indexPath.section && $0.row == indexPath.row })

    if filteredCachedImages.count > 0 {
        let firstItem = filteredCachedImages[0]
        return UIImage(contentsOfFile: firstItem.imagePath)!
    }
    else {
        return nil
    }
}

Also use something like this answer in order to avoid blocking the main thread

I made an example: find it here

Solution 5

Use the following code to reduce the size of the image while storing it :

       var newImage : UIImage
       var size = CGSizeMake(400, 300)
       UIGraphicsBeginImageContext(size)
       image.drawInRect(CGRectMake(0,0,400,300))
       newImage = UIGraphicsGetImageFromCurrentImageContext()
       UIGraphicsEndImageContext()

I would suggest to optimize your code instead of creating an array of photos just create an array of the URL's(ios version < 8.1 from AssetLibrary)/localIdentifier(version >8.1 Photos Library) and fetch images only when required through these URL's. i.e. while displaying.

ARC does not handles the memory management properly sometimes in case of storing images in an array and it causes memory leak too at many places.

You can use autoreleasepool to remove the unnecessary references which could not be released by ARC.

To add further, if you capture any image through camera then the size that is stored in the array is far more large than the size of the image(Although i am not sure why!).

Share:
12,546
Josh O'Connor
Author by

Josh O'Connor

Lead iOS developer at Mango Technologies and ClickUp.com. I've been practicing native iOS Development for 4 years, and have prior experience in Android, Web Development, Hybrid Apps, and Information Technology.

Updated on June 13, 2022

Comments

  • Josh O'Connor
    Josh O'Connor almost 2 years

    In my app, I have an image array which holds all the images taken on my camera. I am using a collectionView to display these images. However, when this image array reaches the 20th or so image, it crashes. I believe this is due to a memory issue.. How do I store the images in an image array in a way which is memory efficient?

    Michael Dauterman provided an answer using thumbnail images. I was hoping there was a solution besides this. Maybe storing the pictures into NSData or CoreData?

    Camera.swift:

    //What happens after the picture is chosen
    func imagePickerController(picker:UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject:AnyObject]){
        //cast image as a string
        let mediaType = info[UIImagePickerControllerMediaType] as! NSString
        self.dismissViewControllerAnimated(true, completion: nil)
        //if the mediaType it actually is an image (jpeg)
        if mediaType.isEqualToString(kUTTypeImage as NSString as String){
            let image = info[UIImagePickerControllerOriginalImage] as! UIImage
    
            //Our outlet for imageview
            appraisalPic.image = image
    
            //Picture taken, to be added to imageArray
            globalPic = image
    
            //image:didFinish.. if we arent able to save, pass to contextInfo in Error Handling
            if (newMedia == true){
                UIImageWriteToSavedPhotosAlbum(image, self, "image:didFinishSavingWithError:contextInfo:", nil)
    
            }
        }
    }
    

    NewRecord.swift

    var imageArray:[UIImage] = [UIImage]()
    viewDidLoad(){
    
        //OUR IMAGE ARRAY WHICH HOLDS OUR PHOTOS, CRASHES AROUND 20th PHOTO ADDED
        imageArray.append(globalPic)
    
    //Rest of NewRecord.swift is code which adds images from imageArray to be presented on a collection view
    }
    
  • Josh O'Connor
    Josh O'Connor over 8 years
    How would I get the full size image from the camera roll?
  • Michael Dautermann
    Michael Dautermann over 8 years
    There's probably a solution in this related question that would help you.
  • Josh O'Connor
    Josh O'Connor over 8 years
    Thank you. I might do that. Would storing the photos as NSData be another solution?
  • Josh O'Connor
    Josh O'Connor over 8 years
    Is there a way to when the picture is taken, a lower quality image is taken rather than a full quality? Before my image is added to my array.
  • Josh O'Connor
    Josh O'Connor over 8 years
    Are you suggesting I store each image in CoreData?
  • zhubofei
    zhubofei over 8 years
    @JoshO'Connor Yes, and use GCD to do it in background. You shouldn't just save images in memory anyway, after the app is closed manually or crashes the image will be gone forever.
  • Josh O'Connor
    Josh O'Connor over 8 years
    Sounds good. Can you provide sample code or a project I can use for reference? I've never used CoreData before. Still learning.
  • zhubofei
    zhubofei over 8 years
    @Josh O'Connor Example code for saving image into CoreData link
  • zhubofei
    zhubofei over 8 years
    @Josh O'Connor This is a tutorial on how to setup CoreData link
  • Josh O'Connor
    Josh O'Connor over 8 years
    Thank you. Also, if I am retrieving images from an external database (Parse) that I want displayed, would I store all these into CoreData as well and do the same?
  • zhubofei
    zhubofei over 8 years
    Parse has already built local datastore into its SDK parse.com/docs/ios/guide#local-datastore. You should defiantly use their solution instead of writing it by yourself. And there is also a useful ParseUI pod github.com/ParsePlatform/ParseUI-iOS you can check out. It includes a PFQueryCollectionViewController as well as a PFImageView class which helps you handle image retrieving.
  • Josh O'Connor
    Josh O'Connor over 8 years
    Thank you! I believe this is the answer I am looking for... Question: Is the memory warning due the pictures being displayed? Or the pictures being stored to the image array? Aka, if I don't have these pictures displayed at all I should or should not have a memory issue...
  • Josh O'Connor
    Josh O'Connor over 8 years
    I will work on this when I am free later and if it is the solution I will give you your bounty.
  • dGambit
    dGambit over 8 years
    The memory warning is due to all objects in memory, even if they're not displayed, for example, if you have a reference to an image in an array it will occupy space. The purpose of that method (didReceiveMemoryWarning()) is very clear: // Dispose of any resources that can be recreated. :D
  • Josh O'Connor
    Josh O'Connor over 8 years
    Thank you! I will try this out tomorrow. Sorry it is taking so long to test your answer and award a bountie, Ive been up to my elbows in work. Much appreciated! :D
  • Josh O'Connor
    Josh O'Connor over 8 years
    Hey dGambit. I have been working on this all day and I am not able to successfully save the picture using the saveImage function. Do you have any sample projects which do this which I can take a look at?
  • Josh O'Connor
    Josh O'Connor over 8 years
    I tried using Haneke, it seems like the solution but the documentation has gotten me lost. In your code above you include "SDImageCache" and "initWithNamespace", two things which are not talked about at all on the github documentation. When I put your code it, it gives me tons of errors, such as "Use of undeclared type, SDImageCache". How am I supposed to know about code like this if it isn't mentioned on the docs?
  • Josh O'Connor
    Josh O'Connor over 8 years
    I would like to use Haneke but I have no idea how to go about using it.
  • dGambit
    dGambit over 8 years
    I edited the answer and added at the bottom a google drive file :D
  • Jiri Trecak
    Jiri Trecak over 8 years
    Then you need to look into how to use libraries in general - I suspect your problem is there
  • Josh O'Connor
    Josh O'Connor over 8 years
    LOL. Thank you for your help for putting me down and offering no advice!!