Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UICollectionView Cell Image changing as it comes into view with GCD

I need to resize a large locally stored image (contained in self.optionArray) and then show it in the collectionView. If I just show it, iOS trying to resize the images as I scroll quickly causing memory-related crashes.

In the code below, the collectionView will scroll smoothly, but sometimes if I scroll extremely fast, there will be an incorrect image that shows and then changes to the correct one as the scrolling decelerates. Why isn't setting the cell.cellImage.image to nil fixing this?

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

    CustomTabBarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CustomTabBarCell" forIndexPath:indexPath];
    cell.cellImage.image = nil;
            dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);

            dispatch_async(queue, ^{
                cell.cellImage.image = nil;
                UIImage *test = [self.optionArray objectAtIndex:indexPath.row];
                UIImage *localImage2 = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];

                dispatch_sync(dispatch_get_main_queue(), ^{

                    cell.cellImage.image = localImage2
                    cell.cellTextLabel.text = @"";
                    [cell setNeedsLayout];
                });

            });

        }

    return cell;
    }

- (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize {
    UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
    [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
}

EDIT: I added another async to cache first and nil and initialized the cell.image. I'm having the same issue on the initial fast scroll down. However, on the scroll back up, it's flawless now.

I added this:

-(void)createDictionary
{
    for (UIImage *test in self.optionArray) {
        UIImage *shownImage = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];
        [localImageDict setObject:shownImage forKey:[NSNumber numberWithInt:[self.optionArray indexOfObject:test]]];
    }
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    if (!localImageDict) {
        localImageDict = [[NSMutableDictionary alloc]initWithCapacity:self.optionArray.count];
    }
    else {
        [localImageDict removeAllObjects];
    }
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);

    dispatch_async(queue, ^{
        [self createDictionary];
    });

}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    CustomTabBarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CustomTabBarCell" forIndexPath:indexPath];
    cell.cellImage.image = nil;
    cell.cellImage.image = [[UIImage alloc]init];

        if ([localImageDict objectForKey:[NSNumber numberWithInt:indexPath.row]]) {
            cell.cellImage.image = [localImageDict objectForKey:[NSNumber numberWithInt:indexPath.row]];
            cell.cellTextLabel.text = @"";
        }
    else {

        cell.cellImage.image = nil;
        cell.cellImage.image = [[UIImage alloc]init];
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);

        dispatch_async(queue, ^{
            UIImage *test = [self.optionArray objectAtIndex:indexPath.row];
            UIImage *shownImage = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];
            [localImageDict setObject:shownImage forKey:[NSNumber numberWithInt:indexPath.row]];

            dispatch_sync(dispatch_get_main_queue(), ^{

                cell.cellImage.image = shownImage;

                cell.cellTextLabel.text = @"";
                [cell setNeedsLayout];
            });

        });
    }

}
return cell;
like image 343
Eric Avatar asked May 22 '13 13:05

Eric


3 Answers

Taking a closer look at your code sample, I can see the source of your memory problem. The most significant issue that jumps out is that you appear to be holding all of your images in an array. That takes an extraordinary amount of memory (and I infer from your need to resize the images that they must be large).

To reduce your app's footprint, you should not maintain an array of UIImage objects. Instead, just maintain an array of URLs or paths to your images and then only create the UIImage objects on the fly as they're needed by the UI (a process that is called lazy-loading). And once the image leaves the screen, you can release it (the UICollectionView, like the UITableView does a lot of this cleanup work for you as long as you don't maintain strong references to the images).

An app should generally only be maintaining UIImage objects for the images currently visible. You might cache these resized images (using NSCache, for example) for performance reasons, but caches will then be purged automatically when you run low in memory.

The good thing is that you're obviously already well versed in asynchronous processing. Anyway, the implementation might look like so:

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

    NSString *filename = [self.filenameArray objectAtIndex:indexPath.row]; // I always use indexPath.item, but if row works, that's great

    UIImage *image = [self.thumbnailCache objectForKey:filename];          // you can key this on whatever you want, but the filename works

    cell.cellImage.image = image;                                          // this will load cached image if found, or `nil` it if not found

    if (image == nil)                                                      // we only need to retrieve image if not found in our cache
    {
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);

        dispatch_async(queue, ^{
            UIImage *test = [UIImage imageWithContentsOfFile:filename];    // load the image here, now that we know we need it
            if (!test)
            {
                NSLog(@"%s: unable to load image", __FUNCTION__);
                return;
            }

            UIImage *localImage2 = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];
            if (!localImage2)
            {
                NSLog(@"%s: unable to convert image", __FUNCTION__);
                return;
            }

            [self.thumbnailCache setObject:localImage2 forKey:filename];   // save the image to the cache

            dispatch_async(dispatch_get_main_queue(), ^{                   // async is fine; no need to keep this background operation alive, waiting for the main queue to respond
                // see if the cell for this indexPath is still onscreen; probably is, but just in case

                CustomTabBarCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];
                if (updateCell)
                {
                    updateCell.cellImage.image = localImage2
                    updateCell.cellTextLabel.text = @"";
                    [updateCell setNeedsLayout];
                }
            });

        });
    }

    return cell;
}

This assumes that you define a class property of thumbnailCache which is a strong reference to a NSCache that you'll initialize in viewDidLoad, or wherever. Caching is a way to get the best of both worlds, load images in memory for optimal performance, but it will be released when you experience memory pressure.

Clearly, I'm blithely assuming "oh, just replace your array of images with an array of image filenames", and I know you'll probably have to go into a bunch of different portions of your code to make that work, but this is undoubtedly the source of your memory consumption. Clearly, you always could have other memory issues (retain cycles and the like), but there's nothing like that here in the snippet you posted.

like image 150
Rob Avatar answered Nov 01 '22 08:11

Rob


I had a similar problem but went about it a different way.

I also had the issue of "pop-in" as images that were loaded async were flashed as they come in until finally the correct one was shown.

One reason this is happening is that the current indexpath for the cell that was initially dequeued did't match the index of the image you are putting into it.

Basically, if you scroll quickly from 0-19 and the cell you want to update is #20 and you want it to show image #20, but it's still loading images 3, 7, 14 asynchronously.

To prevent this, what I did was track two indices; #1) the most recent indexpath that reflects the actual position of the cell and #2) the index corresponding to the image that is actually being loaded async (in this case should actually be the indexpath you are passing into cellforitematindexpath, it gets retained as the async process works through the queue so will actually be "old" data for some of the image loading) .

One way to get the most recent indexpath may be to create a simple method that just returns an NSInteger for the current location of the cell. Store this as currentIndex.

I then put a couple if statements that checked that the two were equal before actually filling in the image.

so if (currentIndex == imageIndex) then load image.

if you put an NSLog(@"CURRENT...%d...IMAGE...%d", currentIndex, imageIndex) before those if statements you can see pretty clearly when the two do not match up and the async calls should exit.

Hope this helps.

like image 1
chuey101 Avatar answered Nov 01 '22 10:11

chuey101


I found the wording of what chuey101 said, confusing. I figured out a way and then realized that chuey101 meant the same.

If it is going to help anyone, images are flashed and changed because of the different threads that are running. So, when you spawn the thread for image operations, its going to get spawned for a specific cell no, say c1. But, at last when you actually load your image into the cell, its going to be the current cell that you are looking at, the one that you scrolled to - say c2. So, when you scrolled to c2, there were c2 threads that were spawned, one fore each cell, as you scrolled. From what I understand, all these threads are going to try loading their images into the current cell, c2. So, you have flashes of images.

To avoid this, you need to actually check that you are loading the image that you want into the cell that you mean to load to. So, get the collectionviewcell indexpath.row before loading image into it (loading_image_into_cell). Also, get the cell for which you spawned off your thread to before you spawn off the thread i.e. in the main thread (image_num_to_load). Now, before loading, check that these two numbers are equal.

Problem solved :)

like image 1
Divya Konda Avatar answered Nov 01 '22 10:11

Divya Konda