iOS 数组、字典易错点总结

307 阅读3分钟

  数组、字典是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 这种内存屏障操作。原子操作本质上也是通过自旋锁实现,但是性能要比一般的锁好的多。