Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to improve performance of UCollectionView containing lots of small images?

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 UICollectionViewCells 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.
like image 809
Rasto Avatar asked Aug 03 '15 10:08

Rasto


4 Answers

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 UITapGestureRecognizerattached 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.

like image 143
Vlad Papko Avatar answered Nov 04 '22 11:11

Vlad Papko


  1. There is no need to fetch image from document directory every time the cell appears.
  2. Once you fetch the image you can save same in NSCache, next time you just have to get image from NSCache instead of fetching it again from document directory.
  3. Create an object for NSCache objCache;
  4. 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]];
                    });
                }
            }];
        });
    }
    
  5. Once you fetch the image, set it in NSCache with path. Next time it is going to check if already downloaded then set from cache only.

If you need any assistance, please let me know.

Thanks!

like image 39
Eliza Avatar answered Nov 04 '22 10:11

Eliza


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!

like image 3
Kevin Avatar answered Nov 04 '22 11:11

Kevin


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

like image 3
sahara108 Avatar answered Nov 04 '22 11:11

sahara108