数组、字典是iOS开发中常用的数据结构,我从crash的角度总结了一些易错点及应对方法:
一、空对象及数组越界
往数组插入空对象,以及对字典进行读写时,key为空都会导致崩溃。
从数组中读取或修改时传入的index参数超出了数组长度,也会引起崩溃。
应对方法:可以对NSMutableArray和NSDictionary增加分类,并添加safeAddObject、safeSetObject:forKey、safeGetObjectForKey: 等方法,在方法里面增加判空和长度判断逻辑,然后替换掉原有的方法即可,代码直接替换或者方法置换都可以。
@implementation NSMutableArray (safe)//+ (void)load//{// [self overrideMethod:@selector(setObject:atIndexedSubscript:) withMethod:@selector(safeSetObject:atIndexedSubscript:)];//}- (void)safeSetObject:(id)obj atIndexedSubscript:(NSUInteger)idx{ if (obj == nil) { return ; }
if (self.count < idx) { return ; } if (idx == self.count) { [self addObject:obj]; } else { [self replaceObjectAtIndex:idx withObject:obj]; }}
- (void)safeAddObject:(id)object{ if (object == nil) { return; } else { [self addObject:object]; }}
- (void)safeInsertObject:(id)object atIndex:(NSUInteger)index{ if (object == nil) { return; } else if (index > self.count) { return; } else { [self insertObject:object atIndex:index]; }}
- (void)safeInsertObjects:(NSArray *)objects atIndexes:(NSIndexSet *)indexs{ if (indexs == nil) { return; } else if (indexs.count!=objects.count || indexs.firstIndex>=objects.count || indexs.lastIndex>=objects.count) { return; } else { [self insertObjects:objects atIndexes:indexs]; }}
- (void)safeRemoveObjectAtIndex:(NSUInteger)index{ if (index >= self.count) { return; } else { [self removeObjectAtIndex:index]; }}
- (void)safeRemoveObject:(id)obj{ if (![self containsObject:obj]) { return; } else { [self removeObject:obj]; }}@end
@implementation NSMutableDictionary(safe)
//+ (void)load//{// [self exchangeMethod:@selector(setObject:forKeyedSubscript:) withMethod:@selector(safeSetObject:forKeyedSubscript:)];
//}
+ (instancetype)safeWithDictionary:(NSDictionary *)dic { if (!dic) { return nil; } return [self dictionaryWithDictionary:dic];}
+ (instancetype)safeDictionaryWithDictionary:(NSDictionary *)dic { if ([dic isKindOfClass:[NSDictionary class]]) { return [NSMutableDictionary dictionaryWithDictionary:dic]; } return [NSMutableDictionary dictionary];}- (void)safeSetObject:(id)obj forKeyedSubscript:(id<NSCopying>)key{ if (!key) { return ; }
if (!obj || [obj isKindOfClass:[NSNull class]]) { return ; } [self safeSetObject:obj forKeyedSubscript:key];}
- (void)safeSetObject:(id)aObj forKey:(id<NSCopying>)aKey{ if (aObj && ![aObj isKindOfClass:[NSNull class]] && aKey) { [self setObject:aObj forKey:aKey]; } else { return; }}
- (id)safeObjectForKey:(id<NSCopying>)aKey{ if (aKey != nil) { return [self objectForKey:aKey]; } else { return nil; }}- (void)safeRemoveObjectForKey:(id<NSCopying>)aKey { if (aKey) { return [self removeObjectForKey:aKey]; }}
- (void)safeAddEntriesFromDictionary:(NSDictionary *)otherDictionary { if ([self isKindOfClass:[NSMutableDictionary class]] && [otherDictionary isKindOfClass:[NSDictionary class]]) { [self addEntriesFromDictionary:otherDictionary]; }}
@end
二、遍历时删除数组中的对象
遍历时删除数组中的对象本质上也是一种数组越界,只是比较隐蔽,没那么容易发现。比如如下的代码:
NSMutableArray *array = [NSMutableArray arrayWithObjects:@(1),@(2),@(3), nil]; for (NSNumber *num in array) { if (num.intValue == 2) { [array removeObject:num]; } }
在删除2之后,数组长度变为了2,因此再访问3的时候,就会因为数组越界发生崩溃,这种情况Xcode也会给出提示
*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSArrayM: 0x600003c58000> was mutated while being enumerated.'
应对方法:
1. 不要直接使用for...in, 可以用传统的for循环反向遍历数组,或者改用enumerate方法遍历,enumerate内部做了相应处理。
2. 遍历前先将数组拷贝一份出来,然后遍历拷贝的数组,要修改的时候修改原数组即可。这种情况相对比较安全,如果修改和遍历的代码不在一个线程运行,也不会发生崩溃。但是要注意copy出来的对象会导致原数组的引用计数增加,如果遍历的方法被调用比较频繁,可能会引发内存泄漏。用mutableCopy则不会有引用计数的问题。
NSMutableArray *array = [NSMutableArray arrayWithObjects:@(1),@(2),@(3), nil]; NSArray *arrayCopy = array.copy; for (NSNumber *num in arrayCopy) { if (num.intValue == 2) { [array removeObject:num]; } }
三、多线程同时读写
一个线程读的同时另一个线程写,或者两个线程同时写,都会导致EXC_BAD_ACCESS错误。
应对方法:
1. 通过将读写的方法派发到GCD串行队列中,来保证按方法先后顺序来执行。
一般读是要实时返回的,写的实时性要求不高,这时可以对读用dispatch_sync, 写用dispatch_barrier_async,这样能保证读是同步返回的,写是异步的,而且是按调用顺序来执行
2. 加锁,适用于对性能要求不高的场景,使用比较简单,这里就不啰嗦了。
3. 原子操作, 如果不想加锁又觉得GCD太麻烦的话,可以使用原子操作,比如OSAtomicCompareAndSwap32 ,传入变量的地址和期望的值以及要修改的值,CPU会保证这个操作是单线程的。 如果是简单的加、减操作,也可以用OSAtomicIncrement32Barrier 这种内存屏障操作。原子操作本质上也是通过自旋锁实现,但是性能要比一般的锁好的多。