Thermometer
Thermometer

Reputation: 2677

Why does my NSMutableArray contain nil on some indices?

Problem:

After I fill an NSMutableArray with objects and try to do something with it, it contains (id)0x0 on some indices. I thought adding nil to an NSMutableArray wasn't possible at all in Objective-C so I am wondering why this happens.

When does this happen?

'Sometimes' unfortunately. It is reproducible by downloading more than ~5000 tiles, just to get the amount high enough for a chance for this to occur. Even with more than 5000 tiles it sometimes goes flawlessly.

Context:

My app has a button which starts a download for map tiles for a specific region. The download happens parallel in background threads and reports back for every tile downloaded.

To allow for canceling the download, in my downloader singleton I have a temporary NSMutableArray which saves a hash from every tile downloaded. After canceling, I can use that list of hashes to delete every saved tile in the database.

Saving the hashes during downloading seems to go fine, but when I actually want to do anything with it (I use [_currentTileHashes copy] to change it to an NSArray to give to the delete method), it throws an NSInvalidArgumentExceptionon that line saying:

-[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[3402]

When I use the debugger to inspect the _currentTileHashes mutable array, I indeed see that one or two of the indices is actually nil or (id)0x0. This screenshot illustrates it:

Debugger shows nil in array

Relevant code:

This code is from the callback for every tile download where it hashes the tile, adds it to the hashes array and calls back to the UI for progress:

- (void)tileCache:(RMTileCache *)tileCache didBackgroundCacheTile:(RMTile)tile withIndex:(NSUInteger)tileIndex ofTotalTileCount:(NSUInteger)totalTileCount {
    DebugLog(@"Cached tile %lu of %lu.", (unsigned long)tileIndex, (unsigned long)totalTileCount);
    if (_currentlyDownloading) {
        float progress = (float)tileIndex / (float)totalTileCount;

        NSDictionary *progressDict = @{@"progress" : [NSNumber numberWithFloat:progress],
                                       @"routeId" : _downloadingRoute.routeId};

        [_currentTileHashes addObject:[RMTileCache tileHash:tile]];

        [[NSNotificationCenter defaultCenter]
         postNotificationName:@"routeTileDownloaded"
         object:progressDict];
    }
}

This is the way the tile gets hashed (this is from the Mapbox iOS SDK):

+ (NSNumber *)tileHash:(RMTile)tile
{
    return [NSNumber numberWithUnsignedLongLong:RMTileKey(tile)];
}

uint64_t RMTileKey(RMTile tile)
{
    uint64_t zoom = (uint64_t)tile.zoom & 0xFFLL; // 8bits, 256 levels
    uint64_t x = (uint64_t)tile.x & 0xFFFFFFFLL;  // 28 bits
    uint64_t y = (uint64_t)tile.y & 0xFFFFFFFLL;  // 28 bits

    uint64_t key = (zoom << 56) | (x << 28) | (y << 0);

    return key;
}

And finally, the code where the exception occurs:

- (void)tileCacheDidCancelBackgroundCache:(RMTileCache *)tileCache {
    DebugLog(@"Finished canceling tile download");

    [tileCache removeAllCachedImagesForTileHashes:[_currentTileHashes copy]];

    [[NSNotificationCenter defaultCenter]
     postNotificationName:@"routeTileDownloadCanceled"
     object:nil];
}

Tested on iOS 8.4, 8.4.1 (iPhone 6) and 7.1 (iPhone 4)

Feel free to ask for more clarification if something is unclear.

Upvotes: 6

Views: 331

Answers (1)

Paulw11
Paulw11

Reputation: 114975

NSMutableArray is not thread safe, so updating an instance from multiple, concurrent, background downloads is likely to lead to corruption in your array - as you are seeing.

I would suggest using @synchronized to guard the array when you update it -

- (void)tileCache:(RMTileCache *)tileCache didBackgroundCacheTile:(RMTile)tile withIndex:(NSUInteger)tileIndex ofTotalTileCount:(NSUInteger)totalTileCount {
    DebugLog(@"Cached tile %lu of %lu.", (unsigned long)tileIndex, (unsigned long)totalTileCount);
    if (_currentlyDownloading) {
        float progress = (float)tileIndex / (float)totalTileCount;

        NSDictionary *progressDict = @{@"progress" : [NSNumber numberWithFloat:progress],
                                       @"routeId" : _downloadingRoute.routeId};
        @synchronized(_currentTileHashes) {
            [_currentTileHashes addObject:[RMTileCache tileHash:tile]];
        }
        [[NSNotificationCenter defaultCenter]
         postNotificationName:@"routeTileDownloaded"
         object:progressDict];
    }
}

Upvotes: 4

Related Questions