iOS 关于数组、字典等数据类型异常操作crash的解决办法
问题的产生
NSString *string = nil;
// 不可变数组
NSArray *array = @[string]; // 初始化中有nil对象
// 可变数组
NSMutableArray *array2 = [NSMutableArray array];
[array2 addObject:string]; // 添加nil对象
// 不可变字典
NSDictionary *dic = @{@"key":string};
// 可变字典
NSMutableDictionary *dic = [NSMutableDictionary dictionary];
[dic setObject:string forKey:@"key"]; // 设置nil对象
上述的几个例子中,都是对数组、字典的异常操作,因为元素中都出现了nil对象,虽然我们可以在添加之前加判断去除为nil的情况,但是如果内容很多,势必会很繁琐,如果有更好的办法帮我们做完这些繁琐的事情岂不是美事?
项目中的问题
笔者项目中有很多下面的写法
NSArray *array = @[string]; // 初始化中有nil对象
NSDictionary *dic = @{@"key":string};
有的添加了三目运算符,去掉了元素为nil的情况,但是开发同伴很多都是没有判断出现的异常情况,所以存在着很多隐患,为此想了一系列的解决方案,如果正好可以提供读者一些思路,想必是极好的
解决方案一:继承
如果项目中有父类的存在,我们可以在父类中做些文章,我们可以一些新增数据操作方法,用来过滤掉一些异常操作(比如跳过nil对象部分)
解决方案二:分类
方案一显然是不理想的,因为项目中可能存在多种父类,情况多变复杂,显然操作性太低
采用分类方式,分别新增NSArray,NSDictionary等分类文件,为其新增操作方法,在方法中过滤掉异常操作
解决方案三:运行时
方案二较方案一有了更高的操作性,可行性,一定程度上解决了异常操作问题,但是依旧存在着不少问题,例如,
我们添加分类后,我们以后就必须使用新增的方法来操作数据,对于之前的旧代码依旧未能作出响应,假如全部替换的话,势必会产生不小的工作量,这不是我们想看到的;
另外,@[],@{}这种方式将不再可用,不,系统的部分操作方法都不可用,局限性还是很大的
那么,有没有更为优雅的方式解决上述问题呢?答案是有的,就是使用我们OC强大的运行时
基本思路:
1、使用分类
2、在 + (void)load;方法中捕获系统方法,替换为我们自己的方法
3、在自己的方法中处理掉异常
具体实现
例子1:addObject方法添加nil对象
我们先写一个异常操作
NSString *string = nil;
NSMutableArray *array = [NSMutableArray array];
[array addObject:string];
爆出的错误提示
错误提示我们可以看到,__NSArrayM对象调用了insertObject:atIndex:产生了object cannot be nil的错误,显然易见,addObject方法最终会调用insertObject:atIndex:方法,而对象不能为nil
接下来我们来使用运行时交换方法,处理掉这种情况
@implementation NSMutableArray (safe)
+(void)load{
[self swizze];
}
+(void)swizze{
Method old = class_getInstanceMethod(NSClassFromString(@"__NSArrayM"), @selector(insertObject:atIndex:));
Method new = class_getInstanceMethod(self, @selector(insertObject_safe:atIndex:));
if (!old || !new) {
return;
}
method_exchangeImplementations(old, new); // 交换方法
}
-(void)insertObject_safe:(id)anObject atIndex:(NSUInteger)index{
if (index > self.count || !anObject) {
return; // 过滤到异常部分
}
[self insertObject_safe:anObject atIndex:index];
}
@end
将该分类导入需要的文件中,array添加对象时就不会在出现crash问题了
例子2:数组越界
我们使用不可变数组做例子
NSString *string = nil;
NSArray *array = @[@"0",@"1",@"2"];
NSLog(@"%@",array[5]);
报错情况
报错对象__NSArrayI调用objectAtIndex:出现了越界
同样的
@implementation NSArray (safe)
+(void)load{
[self swizze];
}
+(void)swizze{
Method old = class_getInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(objectAtIndex:));
Method new = class_getInstanceMethod(self, @selector(objectAtIndex_safe:));
if (!old || !new) {
return;
}
method_exchangeImplementations(old, new); // 交换方法
}
-(id)objectAtIndex_safe:(NSUInteger)index{
if (index>=self.count) {
return nil; // 处理异常部分
}
return [self objectAtIndex_safe:index];
}
@end
运行,输出
输出我们看到,当数组越界时,仅仅是返回了 nil
除了上述两个例子,系统中还有很多异常操作,比如数组的插入,替换,字典的setObject、字符串的操作、NSRange等等,都是待处理的部分。
总结
相比于在分类中新增方法,使用运行时捕获对应方法,会更优雅,我们不必再需要大张旗鼓的使用新方法替换旧项目中的系统方法,一劳永逸
优化部分
1、因为我们过滤了异常部分,无法定位错误,我们调试起来异常困难,为此,这种过滤方式最好仅仅在release模式下产生作用,而debug模式下依旧需要crash,这点可以使用宏来控制,也可以使用NSAssert断言来控制
写在最后
使用cocoaPods导入相关框架
pod 'SafeOperation'
此框架参考自框架
pod 'SafeKit'
稍作改变,加入了断言方便debug模式下调试,删除了.h文件,作用全局,仅需导入框架即可,无需其他设置