2

I have a View Controller with a property galleryCache and when an image is downloaded using GCD and imageWithData: the image is added to the cache successfully with a key. However, when the view controller is dismissed it keeps strong pointers to those downloaded images causing them not to be removed from memory. Even if I use the removeAllObjects method on the cache in viewDidDisappear: memory does not clear up.

Does anyone know why this might be?

Here is the code for the method which downloads the images.

- (void)imageForFootageSize:(FootageSize)footageSize withCompletionHandler:(void (^)(UIImage *image))completionBlock
{
    if (completionBlock) {
        __block UIImage *image;

        //  Try getting local image from disk.
        //
        __block NSURL *imageURL = [self localURLForFootageSize:footageSize];

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            image = [UIImage imageWithData:[NSData dataWithContentsOfURL:imageURL]];

            dispatch_async(dispatch_get_main_queue(), ^{
                if (image) {
                    completionBlock(image);
                } else {
                    //
                    //  Otherwise try getting remote image.
                    //
                    imageURL = [self remoteURLForFootageSize:footageSize];

                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                        NSData *imageData = [NSData dataWithContentsOfURL:imageURL];

                        dispatch_async(dispatch_get_main_queue(), ^{
                            image = [UIImage imageWithData:imageData];

                            if (image) {
                                //
                                //  Save remote image to disk
                                //
                                NSURL *photoDirectoryURL = [Footage localURLForDirectory];

                                //      Create the folder(s) where the photos are stored.
                                //
                                [[NSFileManager defaultManager] createDirectoryAtPath:[photoDirectoryURL path] withIntermediateDirectories:YES attributes:nil error:nil];

                                //      Save photo
                                //
                                NSString *localPath = [[self localURLForFootageSize:footageSize] path];
                                [imageData writeToFile:localPath atomically:YES];
                            }

                            completionBlock(image);
                        });
                    });
                }
            });
        });
    }
}

Methods which use the above class method to fetch and process the UIImage in the completionHandler.

Method inside UICollectionViewCell subclass.

- (void)setPhoto:(Photo *)photo withImage:(UIImage *)image
{
    [self setBackgroundColor:[UIColor blackColor]];
    [self.imageView setBackgroundColor:[UIColor clearColor]];

    if (photo && !image) {
        [photo imageForFootageSize:[Footage footageSizeThatBestFitsRect:self.bounds]
             withCompletionHandler:^(UIImage *image) {
                 if ([self.delegate respondsToSelector:@selector(galleryPhotoCollectionViewCell:didLoadImage:)]) {
                     [self.delegate galleryPhotoCollectionViewCell:self didLoadImage:image];
                 }

                 image = nil;
             }];
    }

    [self.imageView setImage:image];

    BOOL isPhotoAvailable = (BOOL)(image);

    [self.imageView setHidden:!isPhotoAvailable];
    [self.activityIndicatorView setHidden:isPhotoAvailable];
}

Method in UICollectionView data source delegate

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    DIGalleryPhotoCollectionViewCell *photoCell = [collectionView dequeueReusableCellWithReuseIdentifier:photoCellIdentifier forIndexPath:indexPath];

    [photoCell setDelegate:self];

    Footage *footage = [self footageForIndexPath:indexPath];
    Photo *photo = ([footage isKindOfClass:[Photo class]]) ? (Photo *)footage : nil;

    if (photo) {
        //
        //  Photo
        //
        [photoCell setPhoto:photo withImage:[self.galleryCache objectForKey:photo.footageID]];
    }

    return photoCell;
}

Here are the other relevant methods:

- (void)galleryPhotoCollectionViewCell:(DIGalleryPhotoCollectionViewCell *)cell didLoadImage:(UIImage *)image
{
    NSIndexPath *indexPath = [self.galleryCollectionView indexPathForCell:cell];

    Footage *footage = [self footageForIndexPath:indexPath];

    if ([footage isKindOfClass:[Footage class]]) {
        Photo *photo = (Photo *)footage;

        UIImage *cachedImage = [self.galleryCache objectForKey:photo.footageID];

        if (!cachedImage) {
            cachedImage = image;

            [self.galleryCache setObject:image forKey:photo.footageID];
        }

        [cell setPhoto:photo withImage:image];
    }
}

And also my getter method for the NSCache property galleryCache

- (NSCache *)galleryCache
{
    if (!_galleryCache) {
        _galleryCache = [[NSCache alloc] init];
    }

    return _galleryCache;
}

EDIT

Here is a snapshot of Instruments showing the retain count history of one of the NSCache once its owner (a View Controller) is dismissed.

Adam Carter
  • 4,741
  • 5
  • 42
  • 103
  • possible duplicate of [Caching with UIImage and downloaded images](http://stackoverflow.com/questions/23033475/caching-with-uiimage-and-downloaded-images) – Rich Apr 13 '14 at 16:55

2 Answers2

0

I'm not seeing anything obvious here, though I'd suggest putting a breakpoint where you purge the cache and make sure that's actually happening like you think it is.

If you still don't find it, you can run Allocations tool in Instruments and turn on "record reference counts" (see latter part of this answer, iOS app with ARC, find who is owner of an object), and you can find out precisely where your lingering strong reference is, at which point you can tackle the remediation.

The other obvious solution is to eliminate all of this code and use a proven image caching tool, like SDWebImage which does a lot of the memory and persistent storage caching for you. It's a pretty decent implementation.

Community
  • 1
  • 1
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thanks for the answer, I've update my question with a screenshot of Instruments and one of the image's retain count history. – Adam Carter Apr 13 '14 at 17:47
  • Scratch that. It seems the problem is with the cache object itself not being released. See updated question – Adam Carter Apr 13 '14 at 17:49
  • @AdamCarter Your cache doesn't happen to be defined as a `static` or as a global variable is it? Perhaps you can share your `viewDidDisappear`, too. Did you confirm that that was getting called? – Rob Apr 13 '14 at 18:57
  • See answer above. Worked out the issue – Adam Carter Apr 13 '14 at 23:53
  • it took a while but the answer was much simpler than I thought! :) – Adam Carter Apr 14 '14 at 00:24
0

OK, so after re examining my own code and re examining properties for the billionth x n time, it turns out my error was assigning the delegate property as a 'strong' type. Lesson learned: ALWAYS set delegates as WEAK.

I will definitely have to learn more about Instruments, however.

Adam Carter
  • 4,741
  • 5
  • 42
  • 103