为什么我的 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];
    }
}