In my iOS app I have UICollectionView
that displays around 1200 small (35x35 points) images. The images are stored in application bundle.
I am correctly reusing UICollectionViewCell
s but still have performance problems that vary depending on how I address image loading:
My app is application extension and those have limited memory (40 MB in this case). Putting all 1200 images to Assets catalog and loading them using UIImage(named: "imageName")
resulted in memory crashes - system cached images which filled up the memory. At some point the app needs to allocate bigger portions of memory but these were not available because of cached images. Instead of triggering memory warning and cleaning the cache, operating system just killed the app.
I changed the approach to avoid images caching. I put images to my project (not to asssets catalog) as png files and I am loading them using NSBundle.mainBundle().pathForResource("imageName", ofType: "png")
now. The app no longer crashes due to memory error but loading of single image takes much longer and fast scrolling is lagging even on the newest iPhones.
I have full controll over the images and can transform them for example to .jpeg or optimize them (I already tried ImageOptim and some other options without success).
How can I resolve both these performance problems at once?
EDIT 1:
I also tried loading images in background thread. This is code from my subclass of UICollectionViewCell
:
private func loadImageNamed(name: String) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { [weak self] () in
let image = bundle.pathForResource(name, ofType: "png")?.CGImage
if name == self?.displayedImageName {
dispatch_async(dispatch_get_main_queue(), {
if name == self?.displayedImageName {
self?.contentView.layer.contents = image
}
})
}
})
}
This makes scrolling smooth without consuming additional memory for caching but when scrolling to some location programatically (for example when UICollectionView
scrolls to top) it causes another problem: During scrolling animation the images do not update (scroll is too fast for them to load) and after scrolling is finished it takes wrong images are displayed for fraction of second - and one after another replaced with correct ones. This is very disturbing visually.
EDIT 2:
I cannot group small images into bigger composed images and display those as suggested by this answer.
Reasons:
I can propose alternative way that probably can solve your problem:
Consider to render blocks of images to single composed images. Such large image should cover size of app window. For user it will be looked like collection of small images, but technically it will be table of large images.
Your's current layout:
| | | |
| cell | cell | cell | -> cells outside of screen
| | | |
************************
*| | | |*
*| cell | cell | cell |* -> cells displayed on screen
*| | | |*
*----------------------*
*| | | |*
*| cell | cell | cell |* -> cells displayed on screen
*| | | |*
*----------------------*
*| | | |*
*| cell | cell | cell |* -> cells displayed on screen
*| | | |*
************************
| | | |
| cell | cell | cell | -> cells outside of screen
| | | |
Proposed layout:
| |
| cell with |
| composed image | -> cell outside of screen
| |
************************
*| |*
*| |*
*| |*
*| |*
*| cell with |*
*| composed image |* -> cell displayed on screen
*| |*
*| |*
*| |*
*| |*
*| |*
************************
| |
| cell with |
| composed image | -> cell outside of screen
| |
Ideally if you pre-render such composed images and put them to project at build time, but you can also render them in runtime. For sure first variant will work much more faster. But in any case single large image costs less memory then separate pieces of that image.
If you have possibility to pre-render them then use JPEG format. In this case probably your first solution (load images with [UIImage imageNamed:]
on main thread) will work good because less memory used, layout is much more simpler.
If you have to render them in runtime then you will need use your current solution (do work in background), and you will still see that image misplacements when quick animation happens, but in this case it will be single misplacement (one image covers window frame), so it should look better.
If you need to know what image (original small image 35x35) user clicked, you can use UITapGestureRecognizer
attached to cell. When gesture is recognized you can use locationInView:
method to calculate correct index of small image.
I can't say that it 100% resolves your issue, but it makes sense to try.
In your cellForItemAtIndexPath, just write down
UIImage *cachedImage = [objCache objectForKey:arr_PathFromDocumentDirectory[indexPath.row]];
if (cachedImage) {
imgView_Cell.image = cachedImage;
} else {
dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(q, ^{
/* Fetch the image from the Document directory... */
[self downloadImageWithURL:arr_PathFromDocument[indexPath.row] completionBlock:^(BOOL succeeded, CGImageRef data, NSString* path) {
if (succeeded) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *img =[UIImage imageWithCGImage:data];
imgView_Cell.image = img;
[objCache setObject:img forKey::arr_PathFromDocument[indexPath.row]];
});
}
}];
});
}
If you need any assistance, please let me know.
Thanks!
Change from PNG to JPEG will not help saving the memory, because when you load an image from file to memory, it's extracted from the compressed data to uncompressed bytes.
And for the performance issue, I would recommend that you load the image asynchronously and update the view by using delegate/block. And keep some images in memory(but not all of them, let's say 100)
Hope this helps!
You should create an queue to load image asynchronously. The best choice is a last in first out queue. You can have look at this LIFOOperationQueue. One important thing is prevent showing wrong image that need to be handle separately. To do that, when you create an operation to load the image, give it the current indexPath as identifier. And then in callback function, check if the given indexPath is visible to update the view
if (self.tableView.visibleIndexPath().containsObject(indexPath) {
cell.imageView.image = img;
}
You should also need to custom the LIFOOperationQueue to have maximum number of task in queue so that it can remove unnecessary task. It is good to set the maximum number of task is 1.5 * numberOfVisibleCell
.
One last thing, you should create load image operation in willDisplayCell
instead of cellForRowAtIndexPath
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With