enumerateObjectsUsingBlock:和for(in)之间的不同行为

时间:2014-03-19 11:47:25

标签: objective-c multithreading nsmutablearray nsarray enumeration

提供此代码

NSMutableArray *array = [NSMutableArray new];
for (int i = 0; i < 10000; i++) {
    [array addObject:@(i)];
}

queue1 = dispatch_queue_create("com.test_enumaration.1", DISPATCH_QUEUE_CONCURRENT);
queue2 = dispatch_queue_create("com.test_enumaration.2", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue1, ^{
    int idx = 0;
    for (NSNumber *obj in array) {
        NSLog(@"[%d] %@", idx, obj);
        idx++;
    }
});

double delayInSeconds = 0.3;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, queue2, ^(void){
    [array removeObjectAtIndex:9000];
    NSLog(@"----");
});

我期待这段代码崩溃,因为在queue2调度的块在某些时候会同时执行到枚举,这将触发断言,即枚举时不能改变数组。实际上,这就是发生的事情。

有趣的部分是当您用for ( in )

替换enumerateObjectsUsingBlock:
NSMutableArray *array = [NSMutableArray new];
for (int i = 0; i < 10000; i++) {
    [array addObject:@(i)];
}

queue1 = dispatch_queue_create("com.test_enumaration.1", DISPATCH_QUEUE_CONCURRENT);
queue2 = dispatch_queue_create("com.test_enumaration.2", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue1, ^{
    [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        NSLog(@"[%d] %@",idx, obj);
    }];
});

double delayInSeconds = 0.3;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, queue2, ^(void){
    [array removeObjectAtIndex:9000];
    NSLog(@"----");
});

在我所有的不同测试中,删除对象的块在枚举的中间执行(我看到@&#34; ----&#34;的打印),有趣的是枚举行为正确打印[8999] 8999然后[9000] 9001

在这种情况下,数组在枚举期间发生突变,而不会触发任何断言。这是预期的行为吗?如果是,为什么?我错过了什么?

1 个答案:

答案 0 :(得分:0)

自快速枚举引入以来,它已成为......快速枚举的首选方法。大多数枚举实现(例如for(in)enumerateObjectsUsingBlock:)都将使用快速枚举。

快速枚举将查看数据的存储方式。在NSMutableArray的情况下,我猜测底层数据存储在几个数据块中;一万个项目数组可以实现为一百个包含100个项目的块,每个块将其数百个项目存储在连续的内存中。对某些程序集的分析表明(至少在某些iOS设备上)该类是作为单个巨型循环缓冲区实现的。无论哪种方式,枚举列表可能包含多个连续的对象块。最终,确切的存储机制是无关紧要的;它是访问到底层连续存储,使得快速枚举比替代方案更好。

通常,枚举应该可以防止列表发生变异。您将始终使用for(in)枚举来查看。显然,enumerateObjectsUsingBlock:的某些实现并不能有效地保证列表在枚举期间不会发生变异。我在尝试的设备上遇到断言故障......但听起来有些设备会破坏这种保护。我猜测NSFastEnumerationState中使用的变异防护不完整,可能只观看一个块而不是整个阵列。

我认为这是enumerateObjectsUsingBlock:中的错误。

此外,可能生成异常的任何代码在定义上都是坏代码:您需要提供一种机制来防止您自己的代码在另一个线程迭代时尝试修改数组在它上面。