問題
熟悉iOS開發(fā)的都知道,如果我們往Array或Dictionary中插入nil,應用就會崩潰。如有下面客戶端代碼:
- (void)testDictionaryNullable {
id nilObj = nil;
// 1. 此處會引起崩潰
NSDictionary *dict = @{@"aa": nilObj, @"bb": @"bb"};
NSLog(@"dict: %@", dict);
NSMutableDictionary *mDict = [NSMutableDictionary new];
mDict[@"aaa"] = nilObj; // 此處不會有問題
[mDict setObject:@"bbb" forKey:@"bbb"];
// 2. 此處會引起崩潰
[mDict setObject:nilObj forKey:@"ccc"];
NSLog(@"mDict: %@", mDict);
}
- (void)testArrayNullable {
id nilObj = nil;
// 3. 此處會引起崩潰
NSArray *array = @[@"aa", nilObj];
NSLog(@"array: %@", array);
NSMutableArray *mArray = [[NSMutableArray alloc] initWithCapacity:2];
// 4. 此處會引起崩潰
mArray[0] = nilObj;
// 5. 此處會引起崩潰
[mArray addObject:nilObj];
[mArray addObject:@"bbb"];
NSLog(@"mArray: %@", mArray);
}
注: mDict[@"aaa"] = nilObj; 系統(tǒng)默認可以進行空值檢查,不會有崩潰問題。所以以后還是推薦使用這種帶下標的方式來寫恐锣。
另外,解析json時舞痰,除了像mantle等json庫會把空值轉(zhuǎn)換為nil土榴,系統(tǒng)或其他第三方庫在解析json時,會把json中的null值轉(zhuǎn)換為NSNull响牛,而如果你沒有好好做檢查的話玷禽,把它當NSString或NSNumber來處理的話,就會出現(xiàn)各種unrecognized selector
異常娃善,如:
- (void)testNSNull {
NSString *nilObj = (NSString *)[NSNull null];
NSLog(@"null:%d", [nilObj length]);
}
解決
當然论衍,我們可以小心地使用Array、Dictionary和NSNull聚磺,對它們進行充分的檢查坯台。但是,我們?yōu)槭裁床荒芟裣到y(tǒng)對NSMutableDictionary使用下標寫法時的處理一樣瘫寝,當插入或設置的值為nil時蜒蕾,我們不插入或設置,這樣可以保證程序不異常焕阿,也可以大量減少對返回數(shù)據(jù)的判斷咪啡。同時,讓對NSNull的任何調(diào)用暮屡,都不報錯撤摸,也可以減少對NSNull的檢查。
我們可以使用method swizzling達到上述效果。method swizzling可以查看參考資料1准夷。下面分別對Dictionary钥飞、Array、NSNull分別進行處理衫嵌。
對Dictionary進行method swizzling
我們首先對Dictionary進行處理读宙。先查看上面客戶端代碼中,第一處引起崩潰的地方楔绞,即構(gòu)造NSDictionary時结闸。我們查看console中的崩潰堆棧,如下:
我們可以在調(diào)用+[NSDictionary dictionaryWithObjects:forKyes:count]
時酒朵,對objects進行空值過濾桦锄,如果是空值的話,不插入字典耻讽。相關(guān)代碼如下:
@implementation NSDictionary (Safe)
+ (void)load {
Method originalMethod = class_getClassMethod(self, @selector(dictionaryWithObjects:forKeys:count:));
Method swizzledMethod = class_getClassMethod(self, @selector(na_dictionaryWithObjects:forKeys:count:));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
+ (instancetype)na_dictionaryWithObjects:(const id [])objects forKeys:(const id <NSCopying> [])keys count:(NSUInteger)cnt {
id nObjects[cnt];
id nKeys[cnt];
int i=0, j=0;
for (; i<cnt && j<cnt; i++) {
if (objects[i] && keys[i]) {
nObjects[j] = objects[i];
nKeys[j] = keys[i];
j++;
}
}
return [self na_dictionaryWithObjects:nObjects forKeys:nKeys count:j];
}
@end
經(jīng)過這樣處理后察纯,客戶端代碼1處的代碼就不會崩潰了。
我們再來看针肥,客戶端代碼中的2處,對NSMutalbeDictionary調(diào)用setObject:forKey:
香伴,同樣慰枕,我們查看崩潰堆棧,如下:
注: iOS中使用了大量的
class cluster
即纲,像NSMutaleDictionary
實例化出來的具帮,實際上是__NSDictionaryM
類,而NSDictionary
實例化出來的低斋,實際上是__NSPlaceholderDictionary
蜂厅。class cluster
的介紹也可以查看參考資料1。
我們可以用method swizzling修改-[__NSDictionaryM setObject:forKey:]
方法,讓它在設值時膊畴,先判斷是否value為空掘猿,為空則不設置。代碼如下:
@implementation NSMutableDictionary (Safe)
+ (void)load {
Class dictCls = NSClassFromString(@"__NSDictionaryM");
Method originalMethod = class_getInstanceMethod(dictCls, @selector(setObject:forKey:));
Method swizzledMethod = class_getInstanceMethod(dictCls, @selector(na_setObject:forKey:));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (void)na_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
if (!anObject)
return;
[self na_setObject:anObject forKey:aKey];
}
@end
對Array進行method swizzling
使用method swizzling對Array的處理唇跨,與Dictionary的處理稠通,差不多。先打印崩潰堆棧买猖,然后把相關(guān)函數(shù)進行method swizzling改橘。代碼如下:
@implementation NSArray (Safe)
+ (void)load {
Method originalMethod = class_getClassMethod(self, @selector(arrayWithObjects:count:));
Method swizzledMethod = class_getClassMethod(self, @selector(na_arrayWithObjects:count:));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
+ (instancetype)na_arrayWithObjects:(const id [])objects count:(NSUInteger)cnt {
id nObjects[cnt];
int i=0, j=0;
for (; i<cnt && j<cnt; i++) {
if (objects[i]) {
nObjects[j] = objects[i];
j++;
}
}
return [self na_arrayWithObjects:nObjects count:j];
}
@end
@implementation NSMutableArray (Safe)
+ (void)load {
Class arrayCls = NSClassFromString(@"__NSArrayM");
Method originalMethod1 = class_getInstanceMethod(arrayCls, @selector(insertObject:atIndex:));
Method swizzledMethod1 = class_getInstanceMethod(arrayCls, @selector(na_insertObject:atIndex:));
method_exchangeImplementations(originalMethod1, swizzledMethod1);
Method originalMethod2 = class_getInstanceMethod(arrayCls, @selector(setObject:atIndex:));
Method swizzledMethod2 = class_getInstanceMethod(arrayCls, @selector(na_setObject:atIndex:));
method_exchangeImplementations(originalMethod2, swizzledMethod2);
}
- (void)na_insertObject:(id)anObject atIndex:(NSUInteger)index {
if (!anObject)
return;
[self na_insertObject:anObject atIndex:index];
}
- (void)na_setObject:(id)anObject atIndex:(NSUInteger)index {
if (!anObject)
return;
[self na_setObject:anObject atIndex:index];
}
@end
對NSNull進行處理
對NSNull的unrecognized selector
,可以運用 runtime 的知識玉控,在forwarding時飞主,對無法識別的selector,不拋出異常。相關(guān)知識可以參考資料2碌识。代碼如下:
@implementation NSNull (Safe)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
NSMethodSignature *signature = [super methodSignatureForSelector:sel];
if (!signature) {
signature = [NSMethodSignature signatureWithObjCTypes:@encode(void)];
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
}
總結(jié)
本文使用method swizzling讽挟,對Dictionary、Array進行處理丸冕,使它們在處理空值時耽梅,也不會異常而導致程序崩潰。
對NSNull胖烛,重寫forwarding相關(guān)函數(shù)眼姐。使得當json解析成NSNull時,就算把它當成其他類處理佩番,也不會拋異常而導致程序崩潰众旗。
相關(guān)代碼,已經(jīng)上傳github