Memory issue when using large UIImage array causes crashing (swift)
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!).
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, 2022Comments
-
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 over 8 yearsHow would I get the full size image from the camera roll?
-
Michael Dautermann over 8 yearsThere's probably a solution in this related question that would help you.
-
Josh O'Connor over 8 yearsThank you. I might do that. Would storing the photos as NSData be another solution?
-
Josh O'Connor over 8 yearsIs 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 over 8 yearsAre you suggesting I store each image in CoreData?
-
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 over 8 yearsSounds good. Can you provide sample code or a project I can use for reference? I've never used CoreData before. Still learning.
-
zhubofei over 8 years@Josh O'Connor Example code for saving image into CoreData link
-
zhubofei over 8 years@Josh O'Connor This is a tutorial on how to setup CoreData link
-
Josh O'Connor over 8 yearsThank 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 over 8 yearsParse 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 over 8 yearsThank 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 over 8 yearsI will work on this when I am free later and if it is the solution I will give you your bounty.
-
dGambit over 8 yearsThe 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 over 8 yearsThank 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 over 8 yearsHey 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 over 8 yearsI 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 over 8 yearsI would like to use Haneke but I have no idea how to go about using it.
-
dGambit over 8 yearsI edited the answer and added at the bottom a google drive file :D
-
Jiri Trecak over 8 yearsThen you need to look into how to use libraries in general - I suspect your problem is there
-
Josh O'Connor over 8 yearsLOL. Thank you for your help for putting me down and offering no advice!!