Demo project: NSDictionary-NilSafe

问题

相信用 Objective-C 开发 iOS 应用的人对下面的 crash 不会陌生:

  • *** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[1]
  • *** setObjectForKey: key cannot be nil
  • *** setObjectForKey: object cannot be nil

Objective-C 里的 NSDictionary 是不支持 nil 作为 key 或者 value 的。但是总会有一些地方会偶然往 NSDictionary 里插入 nil value。在我们的项目开发过程中,有两个很常见的场景:

  1. 记 event log(button click 或者 page impression 之类)的时候,比如:
[Logging log:SOME_PAGE_IMPRESSION_EVENT eventData:@{
@"some_value": someObject.someValue,
}];www.90168.org
  1. 发 API request 的时候,比如:
NSDictionary *params = @{
@"some_key": someValue,
};
[[APIClient sharedClient] post:someURL params:params callback:callback];

最初,我们的代码里存在很多如下片段:

[Logging log:SOME_PAGE_IMPRESSION_EVENT eventData:@{
@"some_value": someObject.someValue ?: @"",
}];
NSDictionary *params = @{
@"some_key": someValue ?: @"",
};

或者:

NSMutableDictionary *params = [NSMutableDictionary dictionary];
if (someValue) {
params[@"some_key"] = someValue;
}

这样做有几个坏处:

  1. 冗余代码太多
  2. 一不小心就会忘记检查 nil,有些 corner case 只有上线出现 live crash 了才会被发现
  3. 我们的 API 大部分是以 JSON 格式传参的,所以一个 nil 的值不论是传空字符串还是不传,在语义上都不是很正确,甚至还可能会导致一些奇怪的 server bug

所以我们希望 NSDictionary 用起来是这样的:

  1. 插入 nil 的时候不会 crash
  2. 插入 nil 以后它对应的 key 的确存在,且能取到值(NSNull)
  3. 被 serialize 成 JSON 的时候,被转成 null
  4. 让 NSNull 更接近 nil,可以吃任何方法不 crash

测试用例

这个任务很适合测试驱动开发,所以可以把上一节的需求简单转化成以下测试用例:

- (void)testLiteral {
id nilVal = nil;
id nilKey = nil;
id nonNilKey = @"non-nil-key";
id nonNilVal = @"non-nil-val";
NSDictionary *dict = @{
nonNilKey: nilVal,
nilKey: nonNilVal,
};
XCTAssertEqualObjects([dict allKeys], @[nonNilKey]);
XCTAssertNoThrow([dict objectForKey:nonNilKey]);
id val = dict[nonNilKey];
XCTAssertEqualObjects(val, [NSNull null]);
XCTAssertNoThrow([val length]);
XCTAssertNoThrow([val count]);
XCTAssertNoThrow([val anyObject]);
XCTAssertNoThrow([val intValue]);
XCTAssertNoThrow([val integerValue]);
} - (void)testKeyedSubscript {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
id nilVal = nil;
id nilKey = nil;
id nonNilKey = @"non-nil-key";
id nonNilVal = @"non-nil-val";
dict[nonNilKey] = nilVal;
dict[nilKey] = nonNilVal;
XCTAssertEqualObjects([dict allKeys], @[nonNilKey]);
XCTAssertNoThrow([dict objectForKey:nonNilKey]);
} - (void)testSetObject {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
id nilVal = nil;
id nilKey = nil;
id nonNilKey = @"non-nil-key";
id nonNilVal = @"non-nil-val";
[dict setObject:nilVal forKey:nonNilKey];
[dict setObject:nonNilVal forKey:nilKey];
XCTAssertEqualObjects([dict allKeys], @[nonNilKey]);
XCTAssertNoThrow([dict objectForKey:nonNilKey]);
} - (void)testArchive {
id nilVal = nil;
id nilKey = nil;
id nonNilKey = @"non-nil-key";
id nonNilVal = @"non-nil-val";
NSDictionary *dict = @{
nonNilKey: nilVal,
nilKey: nonNilVal,
};
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:dict];
NSDictionary *dict2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
XCTAssertEqualObjects([dict2 allKeys], @[nonNilKey]);
XCTAssertNoThrow([dict2 objectForKey:nonNilKey]);
} - (void)testJSON {
id nilVal = nil;
id nilKey = nil;
id nonNilKey = @"non-nil-key";
id nonNilVal = @"non-nil-val";
NSDictionary *dict = @{
nonNilKey: nilVal,
nilKey: nonNilVal,
};
NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:NULL];
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSString *expectedString = @"{\"non-nil-key\":null}";
XCTAssertEqualObjects(jsonString, expectedString);
}

以上代码在 demo project 里可以找到,改造以前,所有 case 应该都会 fail,改造的目的是让他们都能通过。

Method Swizzling

根据 crash log,dictionary 主要有三个入口传入 nil object:

  1. 字面量初始化一个 dictionary 的时候,会调用 dictionaryWithObjects:forKeys:count:
  2. 直接调用 setObject:forKey 的时候
  3. 通过下标方式赋值的时候,会调用 setObject:forKeyedSubscript:

所以可以通过 method swizzling,把这四个方法(还有 initWithObjects:forKeys:count:,虽然没有发现哪里有调用到它)替换成自己的方法,在 key 为 nil 的时候忽略,在 value 为 nil 的时候,替换为 NSNull 再插入。

其中 setObject:forKey 方法因为是通过 class cluster 实现的,所以实际替换的是 __NSDictionaryM 的方法。

以 dictionaryWithObjects:forKeys:count: 为例:

+ (instancetype)gl_dictionaryWithObjects:(const id [])objects forKeys:(const id<NSCopying> [])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) {
continue;
}
if (!obj) {
obj = [NSNull null];
}
safeKeys[j] = key;
safeObjects[j] = obj;
j++;
}
return [self gl_dictionaryWithObjects:safeObjects forKeys:safeKeys count:j];
}

完整代码参见 GitHub 源文件

引入这个 category 以后,所有测试用例都可以顺利通过了。

NSNull 的安全性

如上修改 NSDictionary 以后,从 dictionary 里拿到 NSNull 的几率就变高了,所以我们希望 NSNull 可以像 nil 一样,接受所有方法调用并且返回 nil/0。

起初,我们用 libextobjc 里的 EXTNil 作为 placeholder 让 null 更安全。后来发觉其实可以参照 EXTNil 的实现直接 swizzle NSNull 本身的方法,让它可以接受所有方法调用:

- (NSMethodSignature *)gl_methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *sig = [self gl_methodSignatureForSelector:aSelector];
if (sig) {
return sig;
}
return [NSMethodSignature signatureWithObjCTypes:@encode(void)];
}www.90168.org - (void)gl_forwardInvocation:(NSInvocation *)anInvocation {
NSUInteger returnLength = [[anInvocation methodSignature] methodReturnLength];
if (!returnLength) {
// nothing to do
return;
} // set return value to all zero bits
char buffer[returnLength];
memset(buffer, 0, returnLength); [anInvocation setReturnValue:buffer];
}

总结

至此,我们解决了第一节中提到的所有问题,有了一个 nil safe 的 NSDictionary。这个方案在实际项目中使用了一年多,效果良好,唯一遇到过的一个坑是往 NSUserDefaults 里写入带 NSNull 的 dictionary 的时候会 crash:Attempt to insert non-property list object。当然这不是这个方案本身带来的问题,解决方法是把 dictionary archive 或者 serialize 成 JSON 后再写入 User Defaults,但是话说回来,复杂的结构体还是考虑从 User Defaults 中拿走吧。

当 NSDictionary 遇见 nil的更多相关文章

  1. NSArray和NSDictionary添加空对象,以及nil和Nil和NULL和NSNull

    因为在NSArray和NSDictionary中nil中有特殊的含义(表示列表结束),所以不能在集合中放入nil值.如要确实需要存储一个表示“什么都没有”的值,可以使用NSNull类. NSNull只 ...

  2. iOS的nil,Null,NSNull的使用

    今天做项目时,在数组里面取值时,发现里面有NSNull的对象,然后用数组里面对应的对象赋值时出现各种问题,总是报错.后面经过研究和查资料,总算解决了这一问题. nil用来给对象赋值(Objective ...

  3. iOS-nil,Nil,NULL的区别

    一.简述 1.nil用来给对象赋值(Objective-C中的任何对象都属于id类型) 2.NULL则给任何指针赋值,NULL和nil不能互换 3.nil用于类指针赋值(在Objective-C中类是 ...

  4. IOS 学习笔记 2015-03-20 O之 nil,Nil,NULL,NSNull

    1.oc最好 用nil   [ nil  任意方法],不会崩溃 nil 是一个对象值.NULL是一个通用指针(泛型指针). 2. NSNULL,NULL和nil在本质上应该是一样的,NULL和nil其 ...

  5. objective-C nil,Nil,NULL 和NSNull的小结

    nil用来给对象赋值(Object-C的任何对象都属于id类型),NULL则给任何指针赋值,NULL和nil不能互换,nil用于类指针赋值(在Object-C中类是一个对象,是类的meta-class ...

  6. OC中的野指针,空指针,nil,Nil,NULL,NSNULL小结

    周末与一个老朋友吃饭聊天,因为他正在培训班学习iOS开发,就随便聊了几句,发现自己OC基础上的欠缺和一些知识点的混淆.特此整理如下. 1.空指针 没有存储任何内存地址的指针就称为空指针(NULL指针) ...

  7. ios nil、NULL和NSNull 的使用

    nil用来给对象赋值(Objective-C中的任何对象都属于id类型),NULL则给任何指针赋值,NULL和nil不能互换,nil用于类指针赋值(在Objective-C中类是一个对象,是类的met ...

  8. NSString json 车NSDictionary

    NSData *jsonContent = [[userInfo objectForKey:@"acme"] dataUsingEncoding:NSUTF8StringEncod ...

  9. nil、Nil、NULL与NSNull的区别及应用

      总结 nil:OC中的对象的空指针 Nil:OC中类的空指针 NULL:C类型的空指针 NSNull:数值类的空对象 详细解析应用如下: 1.nil 指向一个对象的指针为空 在objc.h中的定义 ...

随机推荐

  1. 如何让Table中的第一列和第二列的值相乘然后赋值给第三列

    因为需求的原因所以这样做,不废话了,直接上代码,我用的GridView绑定的数据,table也一样,因为GridView通过浏览器编译后的代码就是table.下面是aspx页面的Html代码: < ...

  2. Caffe初试(一)win7_64bit+VS2013+Opencv2.4.10+CUDA6.5配置Caffe环境

    折腾了几天,终于在windows系统上成功配置了Caffe环境,期间遇到了很多问题,每个问题的解决也都花了不少时间,查过挺多资料,感觉挺有意义,这里写篇博客记录一下. 原来我使用的CUDA版本是7.5 ...

  3. 字符匹配算法之Boyer-Moore算法

    Boyer-Moore算法的精华是从后向前,取好后缀与坏后缀中的最大移动位移动搜索词,以达到最快速检索的效果. 详情参考:http://www.ruanyifeng.com/blog/2013/05/ ...

  4. TCP/IP五层模型

    (2)TCP/IP五层模型的协议   应用层 传输层 网络层 数据链路层 物理层   物理层:中继器.集线器.还有我们通常说的双绞线也工作在物理层 数据链路层:网桥(现已很少使用).以太网交换机(二层 ...

  5. Html和CSS的关系

    1. HTML是网页内容的载体.内容就是网页制作者放在页面上想要让用户浏览的信息,可以包含文字.图片.视频等. 2. CSS样式是表现.就像网页的外衣.比如,标题字体.颜色变化,或为标题加入背景图片. ...

  6. php 会话控制

    会话控制 HTTP协议,在TCP协议基础上的HTTP协议称为无状态协议 SESSION COOKIE SESSION特点:1.存储在服务器.2.每个使用者都会生成一个SESSION.3.有默认的过期时 ...

  7. openfire 服务器名称:后面的黄色叹号

    然后点击重新获取证书,然后重新启动服务,问题解决!

  8. MVC – 9.mvc整体请求流程

    1.请求管道 2~5微软自己的验证,我们一般不用. 在全局配置文件中-已经配置一个路由过滤器-为第7个事件注册了路由方法   1.在application_start中向静态路由表注册了路由数据,在管 ...

  9. 二叉树学习笔记之B树、B+树、B*树

    动态查找树主要有二叉查找树(Binary Search Tree),平衡二叉查找树(Balanced Binary Search Tree), 红黑树 (Red-Black Tree ), 都是典型的 ...

  10. 攻城狮在路上(叁)Linux(二十)--- Linux磁盘格式化

    磁盘完成分区之后,进行格式化,生成文件系统. 命令格式: mkfs [-t 文件系统格式] 设备文件名  <== 使用 mkfs [Tab][Tab] 可以查看linux支持的文件系统格式 示例 ...