Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UICollection View Scroll lag with SDWebImage

Background

I have searched around SO and apple forum. Quite a lot of people talked about performance of collection view cell with image. Most of them said it is lag on scroll since loading the image in the main thread.

By using SDWebImage, the images should be loading in separate thread. However, it is lag only in the landscape mode in the iPad simulator.

Problem description

In the portrait mode, the collection view load 3 cells for each row. And it has no lag or insignificant delay. In the landscape mode, the collection view load 4 cells for each row. And it has obvious lag and drop in frame rate.

I have checked with instrument tools with the core animation. The frame rate drop to about 8fps when new cell appear. I am not sure which act bring me such a low performance for the collection view.

Hope there would be someone know the tricks part.

Here are the relate code

In The View Controller

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    ProductCollectionViewCell *cell=[collectionView dequeueReusableCellWithReuseIdentifier:@"ProductViewCell" forIndexPath:indexPath];

    Product *tmpProduct = (Product*)_ploader.loadedProduct[indexPath.row];

    cell.product = tmpProduct;

    if (cellShouldAnimate) {
        cell.alpha = 0.0;
        [UIView animateWithDuration:0.2
                              delay:0
                            options:(UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction)
                         animations:^{
                            cell.alpha = 1.0;
                         } completion:nil];
    }

    if(indexPath.row >= _ploader.loadedProduct.count - ceil((LIMIT_COUNT * 0.3)))
    {

        [_ploader loadProductsWithCompleteBlock:^(NSError *error){
            if (nil == error) {

                cellShouldAnimate = NO;
                [_collectionView reloadData];
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
                    cellShouldAnimate = YES;
                });
            } else if (error.code != 1){
                #ifdef DEBUG_MODE
                    ULog(@"Error.des : %@", error.description);
                #else
                    CustomAlertView *alertView = [[CustomAlertView alloc]
                                                  initWithTitle:@"Connection Error"
                                                        message:@"Please retry."
                                                   buttonTitles:@[@"OK"]];
                    [alertView show];
                #endif
            }
        }];
    }
    return cell;

}

PrepareForReuse in the collectionViewCell

- (void)prepareForReuse
{
    [super prepareForReuse];
    CGRect bounds = self.bounds;

    [_thumbnailImgView sd_cancelCurrentImageLoad];

    CGFloat labelsTotalHeight = bounds.size.height - _thumbnailImgView.frame.size.height;

    CGFloat brandToImageOffset = 2.0;
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        brandToImageOffset = 53.0;
    }

    CGFloat labelStartY = _thumbnailImgView.frame.size.height + _thumbnailImgView.frame.origin.y + brandToImageOffset;

    CGFloat nameLblHeight = labelsTotalHeight * 0.46;
    CGFloat priceLblHeight = labelsTotalHeight * 0.18;




    _brandLbl.frame = (CGRect){{15, labelStartY}, {bounds.size.width - 30, nameLblHeight}};


    CGFloat priceToNameOffset = 8.0;

    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        priceToNameOffset = 18.0;
    }

    _priceLbl.frame = (CGRect){{5, labelStartY + nameLblHeight  - priceToNameOffset}, {bounds.size.width-10, priceLblHeight}};

    [_spinner stopAnimating];
    [_spinner removeFromSuperview];
    _spinner = nil;

}

Override the setProduct method

- (void)setProduct:(Product *)product
{

    _product = product;

    _spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
    _spinner.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
    [self addSubview:_spinner];
    [_spinner startAnimating];
    _spinner.hidesWhenStopped = YES;

    // Add a spinner

    __block UIActivityIndicatorView *tmpSpinner = _spinner;
    __block UIImageView *tmpImgView = _thumbnailImgView;
    ProductImage *thumbnailImage = _product.images[0];


    [_thumbnailImgView sd_setImageWithURL:[NSURL URLWithString:thumbnailImage.mediumURL]
                                completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
                                    // dismiss the spinner
                                    [tmpSpinner stopAnimating];
                                    [tmpSpinner removeFromSuperview];
                                    tmpSpinner = nil;
                                    if (nil == error) {

                                        // Resize the incoming images
                                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                                            CGFloat imageHeight = image.size.height;
                                            CGFloat imageWidth = image.size.width;

                                            CGSize newSize = tmpImgView.bounds.size;
                                            CGFloat scaleFactor = newSize.width / imageWidth;
                                            newSize.height = imageHeight * scaleFactor;

                                            UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
                                            [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
                                            UIImage *small = UIGraphicsGetImageFromCurrentImageContext();
                                            UIGraphicsEndImageContext();

                                            dispatch_async(dispatch_get_main_queue(),^{
                                                tmpImgView.image = small;
                                            });

                                        });


                                        if (cacheType == SDImageCacheTypeNone) {
                                            tmpImgView.alpha = 0.0;

                                            [UIView animateWithDuration:0.2
                                                                  delay:0
                                                                options:(UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction)
                                                             animations:^{
                                                                 tmpImgView.alpha = 1.0;
                                                             } completion:nil];
                                        }

                                    } else {
                                        // loading error
                                        [tmpImgView setImage:[UIImage imageNamed:@"broken_image_small"]];
                                    }
                                }];

    _brandLbl.text = [_product.brand.name uppercaseString];

    _nameLbl.text = _product.name;
    [_nameLbl sizeToFit];


    // Format the price
    NSNumberFormatter * floatFormatter = [[NSNumberFormatter alloc] init];
    [floatFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
    [floatFormatter setDecimalSeparator:@"."];
    [floatFormatter setMaximumFractionDigits:2];
    [floatFormatter setMinimumFractionDigits:0];
    [floatFormatter setGroupingSeparator:@","];

    _priceLbl.text = [NSString stringWithFormat:@"$%@ USD", [floatFormatter stringFromNumber:_product.price]];

    if (_product.salePrice.intValue > 0) {
        NSString *rawStr = [NSString stringWithFormat:@"$%@ $%@ USD", [floatFormatter stringFromNumber:_product.price], [floatFormatter stringFromNumber:_product.salePrice]];

        NSMutableAttributedString * string = [[NSMutableAttributedString alloc] initWithString:rawStr];
        // Change all the text to red first
        [string addAttribute:NSForegroundColorAttributeName
                       value:[UIColor colorWithRed:157/255.0 green:38/255.0 blue:29/255.0 alpha:1.0]
                       range:NSMakeRange(0,rawStr.length)];

        // find the first space
        NSRange firstSpace = [rawStr rangeOfString:@" "];

        // Change from zero to space to gray color
        [string addAttribute:NSForegroundColorAttributeName
                       value:_priceLbl.textColor
                       range:NSMakeRange(0, firstSpace.location)];

        [string addAttribute:NSStrikethroughStyleAttributeName
                       value:@2
                       range:NSMakeRange(0, firstSpace.location)];

        _priceLbl.attributedText = string;
    }

}
like image 680
Ken Hui Avatar asked Sep 25 '14 08:09

Ken Hui


1 Answers

SDWebImage is very admirable, but DLImageLoader is absolutely incredible, and a key piece of many big production apps

https://stackoverflow.com/a/19115912/294884

it's amazingly easy to use.

To avoid the skimming problem, basically just introduce a delay before bothering to start downloading the image. So, essentially like this...it's this simple

dispatch_after_secs_on_main(0.4, ^
    {
    if ( ! [urlWasThen isEqualToString:self.currentImage] )
        {
        // so in other words, in fact, after a short period of time,
        // the user has indeed scrolled away from that item.
        // (ie, the user is skimming)
        // this item is now some "new" item so of course we don't
        // bother loading "that old" item
        // ie, we now know the user was simply skimming over that item.
        // (just TBC in the preliminary clause above,
        // since the image is already in cache,
        // we'd just instantly load the image - even if the user is skimming)
        // NSLog(@"   --- --- --- --- --- --- too quick!");
        return;
        }

    // a short time has passed, and indeed this cell is still "that" item
    // the user is NOT skimming, SO we start loading the image.
    //NSLog(@"   --- not too quick  ");

    [DLImageLoader loadImageFromURL:urlWasThen
            completed:^(NSError *error, NSData *imgData)
        {
        if (self == nil) return;

        // some time has passed while the image was loading from the internet...

        if ( ! [urlWasThen isEqualToString:self.currentImage] )
            {
            // note that this is the "normal" situation where the user has
            // moved on from the image, so no need toload.
            //
            // in other words: in this case, not due to skimming,
            // but because SO much time has passed,
            // the user has moved on to some other part of the table.
            // we pointlessly loaded the image from the internet!  doh!

            //NSLog(@"  === === 'too late!' image load!");
            return;
            }

        UIImage *image = [UIImage imageWithData:imgData];
        self.someImage.image = image;
        }];

    });

That's the "incredibly easy" solution.

IMO, after vast experimentation, it actually works considerably better than the more complex solution of tracking when the scroll is skimming.

once again, DLImageLoader makes all this extremely easy https://stackoverflow.com/a/19115912/294884


Note that the section of code above is just the "usual" way you load an image inside a cell.

Here's typical code that would do that:

-(void)imageIsNow:(NSString *)imUrl
    {
    // call this routine o "set the image" on this cell.
    // note that "image is now" is a better name than "set the image"
    // Don't forget that cells very rapidly change contents, due to
    // the cell reuse paradigm on iOS.

    // this cell is being told that, the image to be displayed is now this image
    // being aware of scrolling/skimming issues, cache issues, etc,
    // utilise this information to apprporiately load/whatever the image.

    self.someImage.image = nil; // that's UIImageView
    self.currentImage = imUrl; // you need that string property

    [self loadImageInASecIfItsTheSameAs:imUrl];
    }


-(void)loadImageInASecIfItsTheSameAs:(NSString *)urlWasThen
    {

    // (note - at this point here the image may already be available
    // in cache.  if so, just display it. I have omitted that
    // code for simplicity here.)

    // so, right here, "possibly load with delay" the image
    // exactly as shown in the code above .....

    dispatch_after_secs_on_main(0.4, ^
       ...etc....
       ...etc....
    }

Again this is all easily possible due to DLImageLoader which is amazing. It is an amazingly solid library.

like image 160
Fattie Avatar answered Oct 14 '22 11:10

Fattie