0

I have a class method which fetches images with a completion block. This fetched UIImage is added to an NSCache with a relevant key. This seems to work as expected, however, in the method which fetches images I am using a UIImage's imageWithData: method, which I have discovered does not cache it's data, only imageNamed: does.

I am understandably getting memory warnings because of this, how do I make sure the images loaded with UIImage's imageWithData: method are removed from memory when not needed anymore?

EDIT

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);
                        });
                    });
                }
            });
        });
    }
}

EDIT 2

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;
}
Adam Carter
  • 4,741
  • 5
  • 42
  • 103
  • See answer: http://stackoverflow.com/questions/23045412/nscache-holds-strong-pointer-to-uiimage-instantiated-with-imagewithdata-and-doe/23046835#23046835 – Adam Carter Apr 13 '14 at 18:43

1 Answers1

0

Instead of rolling your own image downloading and caching solution you might be better off using SDWebImage. Then you don't have to worry about the downloading, caching or anything. SDWebImage also using disk caching so you don't have to worry about freeing memory.

SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadWithURL:imageURL options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize)
    {
        // progression tracking code
    } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished)
    {
        if (image)
        {
            // do something with image
        }
    }];

I'm not sure but you also might have a retain cycle:

__weak typeof(self) weakSelf = self;
[photo imageForFootageSize:[Footage footageSizeThatBestFitsRect:self.bounds] withCompletionHandler:^(UIImage *image) {
    if ([weakSelf.delegate respondsToSelector:@selector(galleryPhotoCollectionViewCell:didLoadImage:)])
    {
        [weakSelf.delegate galleryPhotoCollectionViewCell:weakSelf didLoadImage:image];
    }
    image = nil;
}];
Rich
  • 8,108
  • 5
  • 46
  • 59
  • Surely there must be a more conventional way of doing this? Would it help if I post my code? – Adam Carter Apr 12 '14 at 17:26
  • Well there's no point reinvent the wheel :) But if you want to do it yourself what is the condition when you don't need the image anymore? – Rich Apr 12 '14 at 17:27
  • As mentioned the image is added to an `NSCache`, and as you can see in the method I use `imageWithData:` which I believe has very bad memory management. As far as I can see I don't need the downloaded `UIImage` as soon as it's downloaded and added to the `NSCache` – Adam Carter Apr 12 '14 at 17:30
  • Where's the `NSCache` in the posted code? If you don't need to show an image set the `UIImageView` `image` to `nil`. As long as you're not retaining the image anywhere it'll be dealloced when its final reference is set to `nil`. – Rich Apr 12 '14 at 17:38
  • The `NSCache` isn't used in that code, its used on a higher level, when that method is run it will add that returned `UIImage` in the `completionHandler:` to an NSCache. My problem is that the returned `UIImage` in the `completionHandler:` does not get released at all. – Adam Carter Apr 12 '14 at 17:46
  • I have added the methods which use the class method `imageForFootageSize:withCompletionHandler:` – Adam Carter Apr 12 '14 at 17:51
  • Try running the static analyser (CMD+Shift+B) - I'm not sure but you might also have a retain cycle in your completion block for `- (void)setPhoto:(Photo *)photo withImage:(UIImage *)image` – Rich Apr 12 '14 at 17:55
  • Also added my delegate method. – Adam Carter Apr 12 '14 at 17:56
  • Worked through a few potential leaks using analyser. Always forget that feature. Will reply with update soon. – Adam Carter Apr 12 '14 at 18:05
  • Despite removing all of the warnings, memory is still never cleaned of `UIImage`s – Adam Carter Apr 12 '14 at 18:08
  • This is where I think the retain cycle is: EDIT, see the answer – Rich Apr 12 '14 at 18:09
  • I've just tried your code but it gives the same results. Any other ideas? – Adam Carter Apr 12 '14 at 18:26
  • How big are you images? – Rich Apr 12 '14 at 18:28
  • From 180x180 up to 1200 x 1200 or more – Adam Carter Apr 12 '14 at 18:30
  • Using your `weakSelf` code along with making the `image` variable (in imageForFootageSize:...) a weak variable does help with memory, however the time it takes to load images is n times slower. – Adam Carter Apr 12 '14 at 18:38
  • I think my issue is inside the delegate method which updates my `NSCache` as it saves the same image into the cache – Adam Carter Apr 12 '14 at 18:50
  • You might also want to change the `image` parameter name to something else in the `withCompletionHandler:^(UIImage *image)` as it matches the method `image` parameter in `- (void)setPhoto:(Photo *)photo withImage:(UIImage *)image` – Rich Apr 12 '14 at 18:55
  • Nice suggestion, unfortunately it's not the solution – Adam Carter Apr 12 '14 at 19:14