#问题
在开发中我们经常会遇到如下问题:

  • failed: caught “NSInvalidArgumentException”, “* -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[1]”
  • failed: caught “NSInvalidArgumentException”, “* setObjectForKey: object cannot be nil (key: no_nillKey)”
  • failed: caught “NSInvalidArgumentException”, “* setObjectForKey: key cannot be nil”

由上可知,Objective-C里的NSDictionary是不支持nil作为key和value的.但是总会有一些地方我们往往会不注意的插入nil值。

通过我们的做法是:

  1. 使用if判断对象是否存在:

    1
    2
    3
    if (obj) {
    [dic setObject:obj forKey:@"obj"];
    }
  2. 使用三目运算符来判断

    1
    [dic setObject:obj?:@"" forKey:@"obj"];

这样做有几个坏处:

  1. 代码冗余较多;
  2. 如果忘了检查nil,在某些情况下就会遇到以上问题;
  3. 后台的API大部分是以JSON 格式传递,所以一个nil值不论是传空字符还是不传都不是很正确。

因此我们希望NSDictionary用起来会遇到以下几种情况:

  1. 插入nil时不会crash

  2. 插入nil值以后(不管是key为nil还是value为nil)对应的key-value 都不包含在NSDictionary

#设计
根据crash 可以看出,dictionary有三个主要的入口传入nil 对象:

  1. 字面量一个dictionary的时候,会调用dictionaryWithObjects:forKeys:count:
  2. 直接调用setObjectForKey:的时候
  3. 通过下标方式赋值的时候(key不能为nil)调用”setObjectForKey: key cannot be nil”

当使用 setObjectForKey时对象是__NSDictionaryM类,同样要定义此类。
因此可以通过Method Swizzing 把这几种方法替换为自己的方法,在key或value 为nil的时候,并不加入到NSDictionary中.

dictionaryWithObjects:forKeys:count为例:

创建交换方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//
// NSDictionary+SafeNil.m
// 当NSDictionary遇到nil
//
// Created by iLogiEMAC on 16/7/11.
// Copyright © 2016年 zp. All rights reserved.
//
#import "NSDictionary+SafeNil.h"
#import <objc/runtime.h>
@implementation NSObject (SafeNil)
+ (BOOL)swizzing_method:(SEL)originalSelector replaceMethod:(SEL)replaceSelector
{
Method original = class_getInstanceMethod(self, originalSelector);
Method replace = class_getInstanceMethod(self, replaceSelector);
if (!original || !replace) {
return NO;
}
class_addMethod(self, originalSelector, class_getMethodImplementation(self, originalSelector), method_getTypeEncoding(original));
class_addMethod(self, replaceSelector, class_getMethodImplementation(self, replaceSelector), method_getTypeEncoding(replace));
method_exchangeImplementations(original, replace);
return YES;
}
+ (BOOL)swizzingClassMethod:(SEL)originSelector replaceMethod:(SEL)replaceSelector
{
return [object_getClass((id)self) swizzing_method:originSelector replaceMethod:replaceSelector];
}
@end

实现自定义的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@implementation NSDictionary (SafeNil)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzingClassMethod:@selector(initWithObjects:forKeys:count:) replaceMethod:@selector(zpSwizzing_initWithObjects:forKeys:count:)];
[self swizzingClassMethod:@selector(dictionaryWithObjects:forKeys:count:) replaceMethod:@selector(zpSwizzingClass_dictionaryWithObjects:forKeys:count:)];
});
}
- (instancetype)zpSwizzing_initWithObjects:(const id _Nonnull __unsafe_unretained *)objects forKeys:(const id<NSCopying> _Nonnull __unsafe_unretained *)keys count:(NSUInteger)cnt{
id safeObjects[cnt];
id safeKeys[cnt];
NSUInteger j = 0;
for (NSUInteger i = 0; i < cnt ; i++) {
id key = keys[i];
id obj = objects[i];
if (!key || !obj) {
continue;
}
safeObjects[j] = obj;
safeKeys[j] = key;
j++;
}
return [self zpSwizzing_initWithObjects:safeObjects forKeys:safeKeys count:j];
}
+ (instancetype)zpSwizzingClass_dictionaryWithObjects:(const id _Nonnull __unsafe_unretained *)objects forKeys:(const id<NSCopying> _Nonnull __unsafe_unretained *)keys count:(NSUInteger)cnt
{
id safeObjects[cnt];
id safeKeys[cnt];
NSUInteger j = 0;
for (NSUInteger i = 0; i < cnt ; i++) {
id key = keys[i];
id obj = objects[i];
if (!key || !obj) {
continue;
}
safeObjects[j] = obj;
safeKeys[j] = key;
j++;
}
return [self zpSwizzingClass_dictionaryWithObjects:safeObjects forKeys:safeKeys count:j];
}
@end

在单元测试中测试:

1
2
3
4
5
6
7
8
9
10
11
12
NSString * no_nillKey = @"no_nillKey";
NSString * nilKey = nil;
NSString * no_nillValue = @"no_nillValue";
NSString * nilValue = nil;
NSDictionary * dic = @{
no_nillKey: nilValue,
nilKey: no_nillValue
};
NSLog(@"%@",dic);
dic 输出为 { }

参考地址