Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Poor UICollectionView Scrolling Performance With UIImage

I have a UICollectionView in my app, and each cell is a UIImageView and some text labels. The problem is that when I have the UIImageViews displaying their images, the scrolling performance is terrible. It's nowhere near as smooth as the scrolling experience of a UITableView or even the same UICollectionView without the UIImageView.

I found this question from a few months ago, and it seems like an answer was found, but it's written in RubyMotion, and I don't understand that. I tried to see how to convert it to Xcode, but since I have never used NSCache either, it's a little hard to. The poster there also pointed to here about implementing something in addition to their solution, but I'm not sure where to put that code either. Possibly because I don't understand the code from the first question.

Would someone be able to help translate this into Xcode?

def viewDidLoad
  ...
  @images_cache = NSCache.alloc.init
  @image_loading_queue = NSOperationQueue.alloc.init
  @image_loading_queue.maxConcurrentOperationCount = 3
  ...
end

def collectionView(collection_view, cellForItemAtIndexPath: index_path)
  cell = collection_view.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER, forIndexPath: index_path)
  image_path = @image_paths[index_path.row]

  if cached_image = @images_cache.objectForKey(image_path)
    cell.image = cached_image
  else
    @operation = NSBlockOperation.blockOperationWithBlock lambda {
      @image = UIImage.imageWithContentsOfFile(image_path)
      Dispatch::Queue.main.async do
        return unless collectionView.indexPathsForVisibleItems.containsObject(index_path)
        @images_cache.setObject(@image, forKey: image_path)
        cell = collectionView.cellForItemAtIndexPath(index_path)
        cell.image = @image
      end
    }
    @image_loading_queue.addOperation(@operation)
  end
end

Here is the code from the second question that the asker of the first question said solved the problem:

UIImage *productImage = [[UIImage alloc] initWithContentsOfFile:path];

CGSize imageSize = productImage.size;
UIGraphicsBeginImageContext(imageSize);
[productImage drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)];
productImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

Again, I'm not sure how/where to implement that.

Many thanks.

like image 989
Nick Avatar asked Apr 03 '13 22:04

Nick


3 Answers

Here's the pattern I follow. Always load asynch and cache the result. Make no assumption about the state of the view when the asynch load finishes. I have a class that simplifies the loads as follows:

//
//  ImageRequest.h

// This class keeps track of in-flight instances, creating only one NSURLConnection for
// multiple matching requests (requests with matching URLs).  It also uses NSCache to cache
// retrieved images.  Set the cache count limit with the macro in this file.

#define kIMAGE_REQUEST_CACHE_LIMIT  100
typedef void (^CompletionBlock) (UIImage *, NSError *);

@interface ImageRequest : NSMutableURLRequest

- (UIImage *)cachedResult;
- (void)startWithCompletion:(CompletionBlock)completion;

@end

//
//  ImageRequest.m

#import "ImageRequest.h"

NSMutableDictionary *_inflight;
NSCache *_imageCache;

@implementation ImageRequest

- (NSMutableDictionary *)inflight {

    if (!_inflight) {
        _inflight = [NSMutableDictionary dictionary];
    }
    return _inflight;
}

- (NSCache *)imageCache {

    if (!_imageCache) {
        _imageCache = [[NSCache alloc] init];
        _imageCache.countLimit = kIMAGE_REQUEST_CACHE_LIMIT;
    }
    return _imageCache;
}

- (UIImage *)cachedResult {

    return [self.imageCache objectForKey:self];
}

- (void)startWithCompletion:(CompletionBlock)completion {

    UIImage *image = [self cachedResult];
    if (image) return completion(image, nil);

    NSMutableArray *inflightCompletionBlocks = [self.inflight objectForKey:self];
    if (inflightCompletionBlocks) {
        // a matching request is in flight, keep the completion block to run when we're finished
        [inflightCompletionBlocks addObject:completion];
    } else {
        [self.inflight setObject:[NSMutableArray arrayWithObject:completion] forKey:self];

        [NSURLConnection sendAsynchronousRequest:self queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
            if (!error) {
                // build an image, cache the result and run completion blocks for this request
                UIImage *image = [UIImage imageWithData:data];
                [self.imageCache setObject:image forKey:self];

                id value = [self.inflight objectForKey:self];
                [self.inflight removeObjectForKey:self];

                for (CompletionBlock block in (NSMutableArray *)value) {
                    block(image, nil);
                }
            } else {
                [self.inflight removeObjectForKey:self];
                completion(nil, error);
            }
        }];
    }
}

@end

Now the cell (collection or table) update is fairly simple:

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {

    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];

    NSURL *url = [NSURL URLWithString:@"http:// some url from your model"];
    // note that this can be a web url or file url

    ImageRequest *request = [[ImageRequest alloc] initWithURL:url];

    UIImage *image = [request cachedResult];
    if (image) {
        UIImageView *imageView = (UIImageView *)[cell viewWithTag:127];
        imageView.image = image;
    } else {
        [request startWithCompletion:^(UIImage *image, NSError *error) {
            if (image && [[collectionView indexPathsForVisibleItems] containsObject:indexPath]) {
                [collectionView reloadItemsAtIndexPaths:@[indexPath]];
            }
        }];
    }
    return cell;
}
like image 77
danh Avatar answered Nov 12 '22 05:11

danh


I had issues about UICollectionView scrolling.

What worked (almost) like a charm for me: I populated the cells with png thumbnails 90x90. I say almost because the first complete scroll is not so smooth, but never crashed anymore.

In my case, the cell size is 90x90.

I had many original png sizes before, and it was very choppy when png original size was greater than ~1000x1000 (many crashes on first scroll).

So, I select 90x90 (or the like) on the UICollectionView and display the original png's (no matter the size). hope it may help others.

like image 20
Paulo Souto Avatar answered Nov 12 '22 05:11

Paulo Souto


In general bad scrolling behaviour for UICollectionViews or UITableViews happens because the cells are dequeued and constructed in the main thread by iOS. There is little freedom to precache cells or construct them in a background thread, instead they are dequeued and constructed as you scroll blocking the UI. (Personally I find this bad design by Apple all though it does simplify matters because you don't have to be aware about potential threading issues. I think they should have given a hook though to provide a custom implementation for a UICollectionViewCell/UITableViewCell pool which can handle dequeuing/reusing of cells.)

The most important causes for performance decrease are indeed related to image data and (in decreasing order of magnitude) are in my experience:

  • Synchronous calls to download image data: always do this asynchronously and call [UIImageView setImage:] with the constructed image when ready in the main thread
  • Synchronous calls to construct images from data on the local file system, or from other serialized data: do this asynchronously as well. (e.g. [UIImage imageWithContentsOfFile:], [UIImage imageWithData:], etc).
  • Calls to [UIImage imageNamed:]: the first time this image is loaded it is served from the file system. You may want to precache images (just by loading [UIImage imageNamed:] before the cell is actually constructed such that they can be served from memory immediately instead.
  • Calling [UIImageView setImage:] is not the fastest method either, but can often not be avoided unless you use static images. For static images it is sometimes faster to used different image views which you set to hidden or not depending on whether they should be displayed instead of changing the image on the same image view.
  • First time a cell is dequeued it is either loaded from a Nib or constructed with alloc-init and some initial layout or properties are set (probably also images if you used them). This causes bad scrolling behaviour the first time a cell is used.

Because I am very picky about smooth scrolling (even if it's only the first time a cell is used) I constructed a whole framework to precache cells by subclassing UINib (this is basically the only hook you get into the dequeuing process used by iOS). But that may be beyond your needs.

like image 25
Werner Altewischer Avatar answered Nov 12 '22 04:11

Werner Altewischer