为什么我的 NSMutableArray 在某些索引上包含 nil?
Why does my NSMutableArray contain nil on some indices?
问题:
在我用对象填充 NSMutableArray
并尝试对其进行操作后,它在某些索引上包含 (id)0x0
。我认为在 Objective-C 中根本不可能将 nil 添加到 NSMutableArray
所以我想知道为什么会这样。
这是什么时候发生的?
'Sometimes' 不幸的是。它可以通过下载超过 ~5000 个图块来重现,只是为了获得足够高的数量以便有机会发生这种情况。即使有 5000 多块瓷砖,它有时也会完美无缺。
上下文:
我的应用程序有一个按钮,可以开始下载特定区域的地图图块。下载在后台线程中并行发生,并报告下载的每个图块。
为了允许取消下载,在我的下载器单例中,我有一个临时的 NSMutableArray
,它保存每个下载的图块的哈希值。取消后,我可以使用该哈希列表删除数据库中保存的每个图块。
在下载过程中保存哈希似乎没问题,但当我真的想用它做任何事情时(我使用 [_currentTileHashes copy]
将其更改为 NSArray
以提供删除方法) ,它在该行上抛出一个 NSInvalidArgumentException
说:
-[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[3402]
当我使用调试器检查 _currentTileHashes
可变数组时,我确实看到一个或两个索引实际上是 nil 或 (id)0x0
。此屏幕截图说明了这一点:
相关代码:
此代码来自每个磁贴下载的回调,它对磁贴进行哈希处理,将其添加到哈希数组并回调 UI 以获取进度:
- (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];
}
}
这是对磁贴进行哈希处理的方式(来自 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;
}
最后,出现异常的代码:
- (void)tileCacheDidCancelBackgroundCache:(RMTileCache *)tileCache {
DebugLog(@"Finished canceling tile download");
[tileCache removeAllCachedImagesForTileHashes:[_currentTileHashes copy]];
[[NSNotificationCenter defaultCenter]
postNotificationName:@"routeTileDownloadCanceled"
object:nil];
}
在 iOS 8.4、8.4.1 (iPhone 6) 和 7.1 (iPhone 4)
上测试
如果有任何不清楚的地方,请随时要求进一步说明。
NSMutableArray
不是线程安全的,因此从多个并发的后台下载更新实例可能会导致阵列损坏 - 如您所见。
我建议您在更新数组时使用@synchronized 来保护它 -
- (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];
}
}
问题:
在我用对象填充 NSMutableArray
并尝试对其进行操作后,它在某些索引上包含 (id)0x0
。我认为在 Objective-C 中根本不可能将 nil 添加到 NSMutableArray
所以我想知道为什么会这样。
这是什么时候发生的?
'Sometimes' 不幸的是。它可以通过下载超过 ~5000 个图块来重现,只是为了获得足够高的数量以便有机会发生这种情况。即使有 5000 多块瓷砖,它有时也会完美无缺。
上下文:
我的应用程序有一个按钮,可以开始下载特定区域的地图图块。下载在后台线程中并行发生,并报告下载的每个图块。
为了允许取消下载,在我的下载器单例中,我有一个临时的 NSMutableArray
,它保存每个下载的图块的哈希值。取消后,我可以使用该哈希列表删除数据库中保存的每个图块。
在下载过程中保存哈希似乎没问题,但当我真的想用它做任何事情时(我使用 [_currentTileHashes copy]
将其更改为 NSArray
以提供删除方法) ,它在该行上抛出一个 NSInvalidArgumentException
说:
-[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[3402]
当我使用调试器检查 _currentTileHashes
可变数组时,我确实看到一个或两个索引实际上是 nil 或 (id)0x0
。此屏幕截图说明了这一点:
相关代码:
此代码来自每个磁贴下载的回调,它对磁贴进行哈希处理,将其添加到哈希数组并回调 UI 以获取进度:
- (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];
}
}
这是对磁贴进行哈希处理的方式(来自 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;
}
最后,出现异常的代码:
- (void)tileCacheDidCancelBackgroundCache:(RMTileCache *)tileCache {
DebugLog(@"Finished canceling tile download");
[tileCache removeAllCachedImagesForTileHashes:[_currentTileHashes copy]];
[[NSNotificationCenter defaultCenter]
postNotificationName:@"routeTileDownloadCanceled"
object:nil];
}
在 iOS 8.4、8.4.1 (iPhone 6) 和 7.1 (iPhone 4)
上测试如果有任何不清楚的地方,请随时要求进一步说明。
NSMutableArray
不是线程安全的,因此从多个并发的后台下载更新实例可能会导致阵列损坏 - 如您所见。
我建议您在更新数组时使用@synchronized 来保护它 -
- (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];
}
}