iOS NSArray枚举
今日在项目开发中遇到一个问题:在职位列表A中点击职位进入职位详细页面B,点击申请职位会到申请职位页面C,申请成功会到申请成功页面D,在D中又有了一个相似职位列表,点击职位又可以进入一个职位详情页面。。。。。。那么问题来了,如果不加限制,那么会导致一个一个的新的B或者C或者D会被push进来。现在要把逻辑改为,点击D的返回按钮,就直接返回的职位列表,不再一级一级地返回。
本来想在D中自定义返回按钮,让其返回的时候pop到指定页面,但是因为项目原因,无法知道是从具体那个页面进来的,也就无法确定要pop到那个页面了。那么就要操作self.navigationViewController的viewControllers数组了。我们需要在进入D的时候将其前面的B、C一类的页面从viewControllers中移除之后再将D加入进来。
最开始我习惯性地用了iOS的快速枚举方法来视图删除B、C这类viewController,但是程序在这个地方奔溃了,没有给任何错误提示,只有这么一句:NSScanner: nil string argument。
NSMutableArray *mutViewControllers = [NSMutableArray arrayWithArray:self.navigationController.viewControllers];
for(UIViewController *tmpVc in mutViewControllers)
{
if([tmpVc isKindOfClass:[NSClassFromString(@"YLJobDetailViewController") class]]||[tmpVc isKindOfClass:[NSClassFromString(@"YLApplyJobViewController") class]])
{
[mutViewControllers removeObject:tmpVc];
}
}
后来尝试了传统的枚举方法,却成功了,如下:
for(NSInteger i=0;i<mutViewControllers.count;i++)
{
UIViewController *tmpVc = mutViewControllers[i];
if([tmpVc isKindOfClass:[NSClassFromString(@"YLJobDetailViewController") class]]||[tmpVc isKindOfClass:[NSClassFromString(@"YLApplyJobViewController") class]]||[tmpVc isKindOfClass:[NSClassFromString(@"YLApplyForInterviewViewController") class]])
{
[mutViewControllers removeObjectAtIndex:i];
i--;
}
}
这是为什么呢?区别在哪里?后面再讨论这个问题!
又因为从D返回到的页面有的是要隐藏tabbar的,有些又不需要隐藏,所以这个地方要加上判断,所以具体实现如下:
NSMutableArray *mutViewControllers = [NSMutableArray arrayWithArray:self.navigationController.viewControllers];
for(NSInteger i=0;i<mutViewControllers.count;i++)
{
UIViewController *tmpVc = mutViewControllers[i];
if([tmpVc isKindOfClass:[NSClassFromString(@"YLJobDetailViewController") class]]||[tmpVc isKindOfClass:[NSClassFromString(@"YLApplyJobViewController") class]]||[tmpVc isKindOfClass:[NSClassFromString(@"YLApplyForInterviewViewController") class]])
{
[mutViewControllers removeObjectAtIndex:i];
i--;
}
}
YLApplyForInterviewViewController *applyInterViewVc = [[YLApplyForInterviewViewController alloc]initWithType:type];
if(type==RegisterSuccessTypeActivate){
applyInterViewVc.userPhone = values[2];
}
YLBaseViewController *lastVc = (YLBaseViewController *)mutViewControllers.lastObject;
BOOL hideBottomBar = lastVc.hidesBottomBarWhenPushed;
if(hideBottomBar==NO)
{
applyInterViewVc.hidesBottomBarWhenPushed = YES;
[lastVc.navigationController pushViewController:applyInterViewVc animated:YES];
}
else
{
[lastVc.navigationController pushViewController:applyInterViewVc animated:YES];
}
[mutViewControllers addObject:applyInterViewVc];
[self.navigationController setViewControllers:mutViewControllers animated:YES];
出现上面的问题主要是对数组的枚举理解的不够深刻。那么我们来看看iOS中的NSArray有哪些枚举方法,它们有什么区别。
================2015-10-10修改===============
上面的修改系统导航的viewcontrollers的例子不太合适,因为在后期的开发中发现这种通过修改系统导航的viewcontrollers的方式来改变导航返回到指定视图控制器的方法在很多时候是容易出bug的。因为在系统导航的push或者pop动画未结束前操作它的viewcontrollers很容易导致程序直接crash,报错信息:
NSScanner:nil string argument
libc++abi.dylib: terminate_handler unexpectedly threw an exception
所以在后期开发中遇到这样的需求还是不推荐这么做,而是在要修改返回按钮事件的视图控制器中自定义返回按钮,然后实现返回按钮的点击事件。
举个栗子:
-(void)backAction
{
UIViewController*targetVc =nil;
if(self.targetPopControllerName){
for(NSIntegeri=self.navigationController.viewControllers.count-1;i>=0;i--){
UIViewController*tmpVc =self.navigationController.viewControllers[i];
if([tmpVcisKindOfClass:NSClassFromString(self.targetPopControllerName)]){
targetVc = tmpVc;
break;
}
}
}
if(targetVc==nil){
[self.navigationControllerpopViewControllerAnimated:YES];
}else{
[self.navigationControllerpopToViewController:targetVcanimated:YES];
}
}
=========================================
好了,下面进入主题:
现有数组:
NSMutableArray *mutArray = [NSMutableArray arrayWithObjects:
@"test",
@"test1",
@"test2",
@"test3",
@"test2",
@"test",
@"test1",
@"test2",
@"test3",
@"test2",
nil];
要将其中的@“test2”全部删除,得到一个新的数组,你能想到几种方法???
//0.最简单的方式
[mutArray removeObject:@“test2"];
这种是最简单的方式,但是往往大家是想不起来的,为什么呢,就是因为我们忽略了字符串的isEqual方法是以它的内容为准的。NSString认为,只要两个字符串的description,也就是字符内容相同,就认为是相同的。(isEqual: 首先判断两个对象是否类型一致, 在判断具体内容是否一致,如果类型不同直接return no.如先判断是否都是 NSString,在判断string的内容。
isEqualToString: 这个直接判断字符串内容,当然你要确保比较的对象保证是字符串。)而数组的removeObject方法是用isEqual去匹配的。再比如要删除所有的@“test2”,@“test3”,可以用[mutArray removeObjectsInArray:@[@“test2”,@"test3"]];这种方法。
但是这种方法也就适合当前这种情况,如果要删除多种数据,就不行了。
//1.传统的下标方法 no pro!
for(NSInteger i=0;i<mutArray.count;i++)
{
NSString *tmpStr = mutArray[i];
if([tmpStr isEqualToString:@"test2"])
{
[mutArray removeObjectAtIndex:i];
i--;
}
}
这种大家应该都能一下子想到,并且这种方式是肯定没有问题的。但是当数组元素很多的时候效率可能就没那么高了。
——————————以下是快速枚举方法——————
//2.快速枚举的时候改变了可变数组 可行吗? 加上__strong如何?
for(NSString *str in mutArray)
{
if([str isEqualToString:@"test2"])
{
[mutArray removeObject:str];
}
}
这种快速枚举方法呢?你会发现即使你加上__strong关键字,依然会报错:Collection <__NSArrayM: 0x7fb6c0439fe0> was mutated while being enumerated
也就是说可变数组在快速枚举的时候不能修改其variables 的引用属性。而我们在这里做了remove操作,所以会异常。
即使改成下面这样依然不行:
for(NSString *str in mutArray)
{
NSLog(@"%@",str);
if([str isEqualToString:@"test2"])
{
NSInteger index = [mutArray indexOfObject:str];
[mutArray removeObjectAtIndex:index];
}
}
这就是在可变数组快速枚举的时候,不能对其做改变操作,因为这样会造成索引错乱的现象。再比如如果数组中全部是字符串的话,这种直接remove操作也会出现一下子将相同的字符串全部移除的误操作。
注意:这种情况下,即使数组中只包含一个@“test2”也不可以!
//3.在枚举的时候枚举[mutArray copy],而在移除的时候操作mutArray 将如何?
for(NSString *str in [mutArray copy])
{
if([str isEqualToString:@"test2"])
{
[mutArray removeObject:str];
}
}
事实证明这种方法是可行的,但是当数组非常大,而需要删除的数据又很少的时候呢?这样就会导致额外地开销了很多内存。
//4.将要删除的数据放进一个新的数组中暂存
NSMutableArray *toDeleteArray = [NSMutableArray array];
for(NSString *str in mutArray)
{
if([str isEqualToString:@"test2"])
{
[toDeleteArray addObject:str];
}
}
[mutArray removeObjectsInArray:toDeleteArray];
这样的方式也可行,但这相当于要循环两次了,先循环一次得到要删除的对象,然后removeObjectsInArray又相当于一次循环删除的操作了。
//5.外部迭代——迭代器 这种可以吗?
NSEnumerator *enumerator=[mutArray objectEnumerator];//得到一个mutArray的枚举对象
NSString *str;
while (str = [enumerator nextObject]) {
if([str isEqualToString:@"test2"])
{
[mutArray removeObject:str];
NSLog(@"xxxx");
}
}
这样的方式呢?报错:Collection <__NSArrayM: 0x7fe180d2ee10> was mutated while being enumerated.’
同样是在枚举的时候修改了数组中对象的引用属性。但是你会发现在NSLog(@“xxxx”);处断点,当程序走到这里的时候,一样是全部删除了@“test2”。这就造成了索引的错乱。
注意:这个地方即使将数组修改为只包含一个@“test2”的数组,同样是不行的!
NSEnumerator *enumerator=[mutArray reverseObjectEnumerator];//得到一个倒序的mutArray的枚举对象
NSString *str;
while (str = [enumerator nextObject]) {
NSLog(@"----%@",str);
if([str isEqualToString:@"test2"])
{
[mutArray removeObject:str];
NSLog(@"xxxx");
}
}
这样翻转过来再迭代呢?会报数组越界错误:-[__NSArrayM objectAtIndex:]: index 8 beyond bounds [0 .. 5]’
我们在NSLog(@“xxxx”);处断点发现,当第一次程序走到断点处的时候,数组中所有的@“test2”全部已经被删除了,这个时候索引并没有却还是8,那就会导致越界。
注意:这个地方如果将数组修改为只包含一个@“test2”的数组,将运行正常。这是因为不会造成索引错乱。
NSEnumerator *enumerator=[mutArray reverseObjectEnumerator];//得到一个倒序的mutArray的枚举对象
NSString *str;
while (str = [enumerator nextObject]) {
NSLog(@"----%@",str);
if([str isEqualToString:@"test2"])
{
NSInteger index = [mutArray indexOfObject:str];
[mutArray removeObjectAtIndex:index];
NSLog(@"xxxx");
}
}
这种方式是可以的。是按照索引一个个删除的,而不是按照对象来删除的。
——————————以下是块枚举——————
//6.块枚举( 并发枚举) 这种可以吗?
[mutArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSString *str = (NSString *)obj;
if([str isEqualToString:@"test2"]){
[mutArray removeObjectAtIndex:idx];
}
}];
这种方式正确,删除了数组中的所有@“test2”数据。在[mutArray removeObjectAtIndex:idx];处断点,可以看到程序走了四次,并且idx是递增的。
这个地方将[mutArray removeObjectAtIndex:idx];换成[mutArray removeObject:str];一样是可以的。而且会发现换成这样当idx=2时将数组中的全部@“test2”删除了,当idx=3时,str对应到了@”test”而不是@”test3”了,它跳过了@“test3”。idx到5就结束了。
这说明在使用数组的removeObject方法时一定要注意数组中元素的isEqual方法!
//这样可以吗?
[mutArray enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSString *str = (NSString *)obj;
if([str isEqualToString:@"test2"]){
[mutArray removeObjectAtIndex:idx];
}
}];
这种 NSEnumerationConcurrent 枚举过程中,各个Block是同时开始执行的。这样枚举的完成顺序是不确定的。在NSString *str = (NSString *)obj;处断点可以idx是不按照顺序来的,也会报数组越界的错误。这就是顺序不确定造成的。
但是注意:这个地方换成[mutArray removeObject:str];反而是可以的。
//这样可以吗?
[mutArray enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSString *str = (NSString *)obj;
if([str isEqualToString:@"test2"]){
[mutArray removeObjectAtIndex:idx];
}
}];
这种 NSEnumerationReverse以反序方式枚举。在NSString *str = (NSString *)obj;处断点可以发现,idx是从大到小递减的,是按顺序来的,不会报错。
这个地方换成[mutArray removeObject:str];一样是可行的,虽然当idx=9的时候已经将所有的@“test2”全部删除,但是当idx=8时,你会发现obj=nil,并没有报数组越界的错误。
使用并发枚举需要注意:
如果情况允许,你可以选择用块枚举来并发枚举对象。这意味着计算的工作量可以分散到几个 CPU 内核上。并不是每种枚举过程中的处理都是可并发的,因此只有没用到锁的时候,才能使用并发枚举:要么每一步操作确实是绝对相互独立的,要么有原子性的操作可用。
总结:
上面说了那么多,说实话我自己都有点绕的傻傻分不清了。。。。。。其实从本文可以看到有几个重点是需要注意的:
1.可变数组在快速枚举的时候不要尝试修改它。
2.需要注意字符串的isEqual:方法和isEqaulToString:方法的区别。
3.在使用数组的removeObject:方法时,你要先搞明白数组中元素的isEqaul:方法,避免误删除。对于自定义类型,可以自定义isEqual:方法。
推荐阅读:
1.http://stackoverflow.com/questions/8834031/objective-c-nsmutablearray-mutated-while-being-enumerated
推荐这篇文章:NSArray 枚举性能研究
大家可以看看,然后在实际项目中根据实际情况选择使用合适的枚举方法。
2.http://www.oschina.net/translate/nsarray-enumeration-performance