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

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

Posted by: admin January 3, 2018 Leave a comment

Questions:

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
}
Answers:

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.

Questions:
Answers:

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.

Questions:
Answers:

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!

Questions:
Answers:

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

Questions:
Answers:

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

Questions:
Answers:

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.

Questions:
Answers:

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)


        }
        }
 }