Massive Memory Leak - CocoaLibSpotify

373 views Asked by At

I'm using the CocoaLibSpotify library to load album art for Spotify search results.

Instruments reports no leaks, and static analysis isn't helping out either, and I've manually reviewed all of my code that deals with keeping track of loading the album art, yet, after loading a few hundred results, the app consumes over 100mb of memory and crashes.

I believe that CocoaLibSpotify is keeping a cache of the images in memory, but there is no way that I have found of disabling the cache. There is a "flushCaches" method, which I've been calling each time I get a memory warning, but, it is ineffective.

Here's what I'm using to load the album art, I keep a reference to all of the SPImage objects in an array, so that I can use them when serving up table view rows.

[self sendRequestToURL: @"http://ws.spotify.com/search/1/track.json" withParams: @{@"q": spotifySearchBar.text} usingMethod: @"GET" completionHandler: ^(id result, NSError *error) {
    //after the search completes, re-enable the search button, replace the searchResults, and
    //  request the result table to reload the data
    spotifySearchBar.userInteractionEnabled = YES;
    [searchBar endEditing: YES];
    [searchResults release];
    int resultLength = [[result objectForKey: @"tracks"] count] < 100 ? [[result objectForKey: @"tracks"] count] : 100;
    searchResults = [[[result objectForKey: @"tracks"] subarrayWithRange: NSMakeRange(0, resultLength)] retain];
    for(int i = 0; i < 100; i++) {
        [albumArtCache replaceObjectAtIndex: i withObject: [NSNull null]];
    }
    for(NSDictionary *trackDict in searchResults) {
        NSString *trackURI = [trackDict objectForKey: @"href"];
        [SPTrack trackForTrackURL: [NSURL URLWithString: trackURI] inSession: session callback: ^(SPTrack *track) {
            [SPAsyncLoading waitUntilLoaded: track timeout: kSPAsyncLoadingDefaultTimeout then:^(NSArray *loadedItems, NSArray *notLoadedItems) {
                if(track == nil) return;
                [SPAsyncLoading waitUntilLoaded: track.album timeout: kSPAsyncLoadingDefaultTimeout then:^(NSArray *loadedItems, NSArray *notLoadedItems) {
                    if(track.album == nil) return;
                    [SPAsyncLoading waitUntilLoaded: track.album.largeCover timeout: kSPAsyncLoadingDefaultTimeout then:^(NSArray *loadedItems, NSArray *notLoadedItems) {
                        if(track.album.largeCover == nil) return;
                        if(![searchResults containsObject: trackDict]) {
                            NSLog(@"new search was performed, discarding loaded result");
                            return;
                        } else{
                            [albumArtCache replaceObjectAtIndex: [searchResults indexOfObject: trackDict] withObject: track.album.largeCover];
                            [resultTableView reloadRowsAtIndexPaths: @[[NSIndexPath indexPathForRow: [searchResults indexOfObject: trackDict] inSection: 0]] withRowAnimation: UITableViewRowAnimationAutomatic];
                        }
                    }];
                }];
            }];
        }];
    }
    [resultTableView reloadData];
}];

And here is the code that deals with loading table view cells.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: @"artistCell"];
    if(cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleSubtitle reuseIdentifier: @"artistCell"] autorelease];
    }
    cell.textLabel.text = [[searchResults objectAtIndex: indexPath.row] objectForKey: @"name"];
    cell.detailTextLabel.text = [[[[searchResults objectAtIndex: indexPath.row] objectForKey: @"artists"] objectAtIndex: 0] objectForKey: @"name"];

    if([albumArtCache objectAtIndex: indexPath.row] != [NSNull null]) {
        cell.imageView.image =  ((SPImage *)[albumArtCache objectAtIndex: indexPath.row]).image;
    } else{
        cell.imageView.image = nil;
    }

    return cell;
}

I really have no idea what's going wrong. Any assistance would be greatly appreciated.

1

There are 1 answers

0
iKenndac On

First off, you should use SPSearch rather than the web API for searching.

The reason that Instruments isn't showing a memory leak is because there isn't one - CocoaLibSpotify caches albums and images internally for performance reasons. As a result, loaded album covers will also stick around.

Now, loading hundreds of 1024x1024 images into memory is obviously going to end badly. An easy way to mitigate the problem would be to not load the largest size image - it's not normally required for a table view at 1024x1024 pixels.

Otherwise, you can modify CocoaLibSpotify to be able to unload images. The easiest way to do this is to probably add a method to SPImage that basically does the opposite of -startLoading - namely, setting the image property to nil, the hasStartedLoading and loaded properties to NO and calling sp_image_release on the spImage property before setting that to NULL.