使用enumerateObjectsUsingBlock和for(in)在遍历数组时的行为有何不同?

4

请使用以下代码

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的块与枚举同时执行,这将触发断言,即在枚举时不能改变数组。事实上,这就是发生的事情。
有趣的部分是当您用enumerateObjectsUsingBlock:替换for ( in )时。
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(@"----");
});

在我进行的各种测试中,删除对象的块在枚举的中间被执行(我看到了"----"的印刷品),有趣的是,枚举行为正确,打印出[8999] 8999,然后是[9000] 9001。 在这种情况下,在枚举期间改变数组,而不触发任何断言。这是一种预期的行为吗?如果是,为什么?我有遗漏什么吗?

可能是 Objective-C enumerateUsingBlock vs fast enumeration? 的重复。 - trojanfoe
@trojanfoe,不完全是这样,在链接的答案中指出“两种方法都可以保护可变集合免受枚举循环内部的变异”,但在我的情况下,我并没有试图在枚举块内更改集合,而是从不同的线程进行了变异。 - Luca Bernardi
@LucaBernardi:当我运行你的代码时,两个变体都会崩溃,并出现“在枚举时修改了集合<__NSArrayM: 0x100113d10>”异常。 - Martin R
我再次运行了测试,对我来说没有崩溃,并且我在枚举的中间看到了“----”被记录,实际输出是[8999] 8999和[9000] 9001。 - Luca Bernardi
我得到了与@LucaBernardi描述的相同输出。实际上,当数组在被枚举时被改变并且简单地跳过被删除的对象时,似乎enumerateObjectsUsingBlock:并不会抱怨。 - andreag
1个回答

0

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

快速枚举将查看数据的存储方式。对于NSMutableArray而言,我猜测底层数据是存储在几个数据块中的;一个有一万项的数组可能被实现为一百个100项的数据块,每个数据块将其100项存储在连续的内存中。对一些汇编代码的分析表明,在某些iOS设备上,该类被实现为单个巨大的循环缓冲区。无论哪种方式,枚举列表可能包含多个连续的对象块。最终,确切的存储机制并不重要;正是对底层连续存储的访问使快速枚举比替代方案更好。

通常,枚举被认为可以防止列表被改变。你总是会看到在for(in)枚举中。显然,一些enumerateObjectsUsingBlock:的实现不能够保证在枚举期间列表不会被改变。我在我尝试过的设备上遇到了断言失败...但听起来有一些设备这种保护机制是失效的。我猜测在NSFastEnumerationState中使用的变异守卫不完整,可能只监视单个块而不是整个数组。

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

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


1
你有任何证据证明NSMutableArray是以块的形式存储数据的吗?这篇文章表明它是一个循环缓冲区。 - vikingosegundo
Mike Ash在https://mikeash.com/pyblog/friday-qa-2010-04-09-comparison-of-objective-c-enumeration-techniques.html中暗示了这一点。Bartosz在你提到的文章中,同样猜测了实际的实现方式。除此之外,实现本身并没有开源,苹果保留了在不同设备和操作系统版本上更改实现的能力。所以,不,我不“知道”它是如何存储的。我会编辑帖子以反映这一点。 - AndrewS
事实上,这里并没有陈述,只有暗示。此外,具体的实现并不重要。重要的是向读者介绍“多个相邻对象块”的概念,我认为这可能是错误的一个可能来源。 - AndrewS
而且这个含义在哪里呢?我找不到它。如果一个数组中有多个块,Bartosz的大部分观察将不可能实现。 - vikingosegundo
使用循环缓冲区允许最多两个不相交的连续对象块...这些块恰好由一组连续的空指针(或未使用的对象存储)连接。至于Mike Ash的文章,请搜索短语“If there are multiple contiguous object stores”。 - AndrewS
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接