問題的產生
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等等,都是待處理的部分。
總結
相比于在分類中新增方法,使用運行時捕獲對應方法,會更優雅,我們不必再需要大張旗鼓的使用新方法替換舊項目中的系統方法,一勞永逸
優化部分
因為我們過濾了異常部分,無法定位錯誤,我們調試起來異常困難,為此,這種過濾方式最好僅僅在release模式下產生作用,而debug模式下依舊需要crash,這點可以使用宏來控制,也可以使用NSAssert斷言來控制
寫在最后
使用cocoaPods導入相關框架
pod 'SafeKit'