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:
- Consider different screen sizes and orientations. There would have to be precomposed images for each of them which would make the app download huge.
- The small images can by displayed in different order, some of them might be hidden in some situation. I surrely cannot have precomposed images for each possible combinations and orders.
To resolve issues that are described in
EDIT 1
you should overrideprepareForReuse
method in theUICollectionViewCell
subclass and reset your layer's content to nil:To load your image in the background you can use the next:
How to improve:
NSCache
or custom caching algorithm in order not to load images while scrolling all the time.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!
Check this tutorial:
http://www.raywenderlich.com/86365/asyncdisplaykit-tutorial-achieving-60-fps-scrolling
It uses AsyncDisplayKit to load images on a background thread.
In your cellForItemAtIndexPath, just write down
If you need any assistance, please let me know.
Thanks!
Couple of things you can do for this,
Other thing when you load image in background decode it in background thread before setting it to image view... by default image will decode when you set and that will happen usually on main thread, this will make scrolling smooth, you can decode an image by code below.
}
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:
Proposed layout:
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 uselocationInView:
method to calculate correct index of small image.I can't say that it 100% resolves your issue, but it makes sense to try.