Reputation: 2677
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.
'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.
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 NSInvalidArgumentException
on 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:
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
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