8

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
}
Alok
  • 24,880
  • 6
  • 40
  • 67
Josh O'Connor
  • 4,694
  • 7
  • 54
  • 98
  • Have you considered holding an array of image paths instead? And then using a collectionView so you can recycle the active views and only load images on an as-needed-basis? – Aggressor Aug 12 '15 at 05:28

7 Answers7

8

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()
maxcodes
  • 544
  • 6
  • 15
Michael Dautermann
  • 88,797
  • 17
  • 166
  • 215
6

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.

zhubofei
  • 112
  • 1
  • 1
  • 10
  • Are you suggesting I store each image in CoreData? – Josh O'Connor Aug 10 '15 at 21:28
  • @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. – zhubofei Aug 10 '15 at 21:35
  • Sounds good. Can you provide sample code or a project I can use for reference? I've never used CoreData before. Still learning. – Josh O'Connor Aug 10 '15 at 21:45
  • @Josh O'Connor Example code for saving image into CoreData [link](http://stackoverflow.com/questions/27995955/saving-picked-image-to-coredata-xcode) – zhubofei Aug 10 '15 at 21:54
  • @Josh O'Connor This is a tutorial on how to setup CoreData [link](http://www.raywenderlich.com/85578/first-core-data-app-using-swift) – zhubofei Aug 10 '15 at 21:58
  • 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? – Josh O'Connor Aug 10 '15 at 22:04
  • Parse has already built local datastore into its SDK https://www.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 https://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. – zhubofei Aug 10 '15 at 22:17
3

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!

Hamzah Malik
  • 2,540
  • 3
  • 28
  • 46
Jiri Trecak
  • 5,092
  • 26
  • 37
  • 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 Aug 16 '15 at 19:10
  • I would like to use Haneke but I have no idea how to go about using it. – Josh O'Connor Aug 16 '15 at 19:10
  • Then you need to look into how to use libraries in general - I suspect your problem is there – Jiri Trecak Aug 17 '15 at 06:33
  • LOL. Thank you for your help for putting me down and offering no advice!! – Josh O'Connor Aug 17 '15 at 06:50
1

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

Community
  • 1
  • 1
dGambit
  • 531
  • 3
  • 11
  • 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 Aug 12 '15 at 18:36
  • I will work on this when I am free later and if it is the solution I will give you your bounty. – Josh O'Connor Aug 12 '15 at 18:38
  • 1
    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 – dGambit Aug 12 '15 at 18:48
  • 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 Aug 13 '15 at 07:08
  • 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 Aug 16 '15 at 18:22
  • I edited the answer and added at the bottom a google drive file :D – dGambit Aug 16 '15 at 22:12
1

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!).

Minkesh Jain
  • 1,140
  • 1
  • 10
  • 24
0

You could just store the raw image data in an array, instead of all the metadata and excess stuff. I don't know if you need metadata, but you may be able to get around without it. Another alternative would be to write each image to a temporary file, and then retrieve it later.

MezuCobalt
  • 60
  • 1
  • 2
  • 11
-3

The best route that worked for me was to store a set of images at full scale is to use the PHPhotoLibrary. PHLibrary comes with caching and garbage collection. The other solutions didn't work for my purposes.

ViewDidLoad:

    //Check if the folder exists, if not, create it
    let fetchOptions = PHFetchOptions()
    fetchOptions.predicate = NSPredicate(format: "title = %@", albumName)
    let collection:PHFetchResult = PHAssetCollection.fetchAssetCollectionsWithType(.Album, subtype: .Any, options: fetchOptions)

    if let first_Obj:AnyObject = collection.firstObject{
        //found the album
        self.albumFound = true
        self.assetCollection = first_Obj as! PHAssetCollection
    }else{
        //Album placeholder for the asset collection, used to reference collection in completion handler
        var albumPlaceholder:PHObjectPlaceholder!
        //create the folder
        NSLog("\nFolder \"%@\" does not exist\nCreating now...", albumName)
        PHPhotoLibrary.sharedPhotoLibrary().performChanges({
            let request = PHAssetCollectionChangeRequest.creationRequestForAssetCollectionWithTitle(albumName)
            albumPlaceholder = request.placeholderForCreatedAssetCollection
            },
            completionHandler: {(success:Bool, error:NSError!)in
                if(success){
                    println("Successfully created folder")
                    self.albumFound = true
                    if let collection = PHAssetCollection.fetchAssetCollectionsWithLocalIdentifiers([albumPlaceholder.localIdentifier], options: nil){
                        self.assetCollection = collection.firstObject as! PHAssetCollection
                    }
                }else{
                    println("Error creating folder")
                    self.albumFound = false
                }
        })

    }



func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject]) {
    //what happens after the picture is chosen
    let mediaType = info[UIImagePickerControllerMediaType] as! NSString
    if mediaType.isEqualToString(kUTTypeImage as NSString as String){
        let image = info[UIImagePickerControllerOriginalImage] as! UIImage

        appraisalPic.image = image
        globalPic = appraisalPic.image!

        if(newMedia == true){
            UIImageWriteToSavedPhotosAlbum(image, self, "image:didFinishSavingWithError:contextInfo:", nil)
            self.dismissViewControllerAnimated(true, completion: nil)
            picTaken = true


            println(photosAsset)


        }
        }
 }
Josh O'Connor
  • 4,694
  • 7
  • 54
  • 98