今天遇到一枚crash,主要错误信息如下:
reason: '*** Collection <__NSArrayM: xxxxxxxx> was mutated while being enumerated.'
大致意思是,集合在枚举时被修改了。于是谷歌一把,发现出错的代码大多形如:
for(item in array) { //难道这里的array用的不是下面修改后的实时array?
if(condition) {
[array removeObject:xxx];
}
}
自己的代码确实有这样的实现,问题是为啥会crash呢?难道for in循环中用的不是实时修改后的array?不能死得不明不白呀!我们知道,oc的底层实现是c,所以看看for in在c层面究竟干了什么,编辑一个demo.m如下:
#import <Foundation/Foundation.h>
int main() {
NSMutableArray *arr = @[@"aaa", @"bbb", @"ccc"];
for (NSString *item in arr) {
if([item isEqualToString:@"bbb"]) {
[arr removeObject:item];
}
}
return 0;
}
接着利用如下命令行,重写demo.m成demo.cpp:
clang -rewrite-objc demo.m // 把m文件重写成cpp文件
demo.cpp的main函数如下:
int main() {
NSMutableArray *arr = ((NSArray *(*)(Class, SEL, const ObjectType *, NSUInteger))(void *)objc_msgSend)(objc_getClass("NSArray"), sel_registerName("arrayWithObjects:count:"), (const id *)__NSContainer_literal(3U, (NSString *)&__NSConstantStringImpl__var_folders_75_sj4g_8kx5kd0d0wvzlhjrnyr0000gn_T_demo_ccc02e_mi_0, (NSString *)&__NSConstantStringImpl__var_folders_75_sj4g_8kx5kd0d0wvzlhjrnyr0000gn_T_demo_ccc02e_mi_1, (NSString *)&__NSConstantStringImpl__var_folders_75_sj4g_8kx5kd0d0wvzlhjrnyr0000gn_T_demo_ccc02e_mi_2).arr, 3U);
{
NSString * item;
struct __objcFastEnumerationState enumState = { 0 };
id __rw_items[16];
id l_collection = (id) arr;
_WIN_NSUInteger limit =
((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend)
((id)l_collection,
sel_registerName("countByEnumeratingWithState:objects:count:"),
&enumState, (id *)__rw_items, (_WIN_NSUInteger)16);
if (limit) {
// startMutations保存mutationsPtr的初始值
unsigned long startMutations = *enumState.mutationsPtr;
do {
unsigned long counter = 0;
do {
// mutationsPtr有变化,而且没有handler,就crash!!!
if (startMutations != *enumState.mutationsPtr)
objc_enumerationMutation(l_collection);
item = (NSString *)enumState.itemsPtr[counter++]; {
if(((BOOL (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)item, sel_registerName("isEqualToString:"), (NSString *)&__NSConstantStringImpl__var_folders_75_sj4g_8kx5kd0d0wvzlhjrnyr0000gn_T_demo_ccc02e_mi_3)) {
((void (*)(id, SEL, ObjectType))(void *)objc_msgSend)((id)arr, sel_registerName("removeObject:"), (id)item);
}
};
__continue_label_1: ;
} while (counter < limit);
} while ((limit = ((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend)
((id)l_collection,
sel_registerName("countByEnumeratingWithState:objects:count:"),
&enumState, (id *)__rw_items, (_WIN_NSUInteger)16)));
item = ((NSString *)0);
__break_label_1: ;
}
else
item = ((NSString *)0);
}
return 0;
}
可以看到,for in的底层实现是嵌套do while循环。注意到上面绿色标注的那行代码,objc_enumerationMutation和mutationsPtr的官方解释如下:
解决方法主要有两种:
1、copy一个数组,遍历副本,原数组removeObject。
2、利用enumerateObjectsUsingBlock block进行遍历,官方推荐,效率更高。
参考链接:
https://developer.apple.com/reference/foundation/nsfastenumerationstate/1408376-mutationsptr?language=objc
https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/Enumeration.html
http://blog.leichunfeng.com/blog/2016/06/20/objective-c-fast-enumeration-implementation-principle/
https://developer.apple.com/reference/objectivec/1418744-objc_enumerationmutation?language=objc
http://blog.csdn.net/piaodang1234/article/details/11902541