Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 960
Thermometer Avatar asked Aug 14 '15 11:08

Thermometer


1 Answers

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];
    }
}
like image 139
Paulw11 Avatar answered Oct 15 '22 14:10

Paulw11