背景
在音频播放的项目中有个需求:监听到播放失败,通过 delegate
的方式把该错误抛到上层。错误信息包含:一个error
和一个音频对象 track
。
下面方法 playerFailedWithError:
是监听 delegate
的地方,请注意注释2
:
- (void)playerFailedWithError:(NSError *)error {
// 1. 通知 delegate 处理错误信息
if ([self.delegate respondsToSelector:@selector(player:didFailedToPlayTrack:withError:)]) {
[self.delegate player:self didFailedToPlayTrack:[self currentTrack] withError:error];
}
// 2. 把错误信息抛到其它监听它的地方,注意 info 属性的设置
NSMutableDictionary *info = [NSMutableDictionary dictionary];
info[kXMPlayerTrack] = [self currentTrack];
info[kXMPlayerError] = error;
[self postSEL:@selector(player:didFailedToPlayTrack:withError:) withUserInfo:info];
}
再给出我接收 delegate
的代码,请注意 body
参数的实现:
// 播放错误回调
- (void)playeFailedWithError:(NSError *)error track:(Track *)track {
[self sendEventWithName:@"onPlayFailed"
body:@{@"error":error,@"track":track}];
}
想想上面代码可能出现的问题
出事了
代码提交后的某次灰度测试,收到了几十条崩溃信息:
Fatal Exception: NSInvalidArgumentException
*** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[1]
Raw Text
0
CoreFoundation
-[__NSPlaceholderDictionary initWithObjects:forKeys:count:]
CoreFoundation
+[NSDictionary dictionaryWithObjects:forKeys:count:]
1
xxx
xxAudioPlayer.m line 16
-[xxAudioPlayer playeFailedWithError:track:]
2
xxx
xxxPlayer.m line 187
-[xxxPlayer playerFailedWithError:]
从调用栈看出崩溃的流程为 playerFailedWithError:
--> playeFailedWithError:track:
-->[NSDictionary dictionaryWithObjects:forKeys:count:]
。
根据上面的信息,定位到代理方法 playeFailedWithError:track:
。回到上面,看一下这个方法的实现,这里面唯一和 NSDictionary
打交道的就是参数 body
: body:@{@"error":error,@"track":track}
。
根据 attempt to insert nil object from objects[1]
,字典里面的 track
值出现了为 nil
的情况!
重新审视 playerFailedWithError: 里面的代码
再次查看方法 playerFailedWithError:
,代理得到的 track
对象是通过 [self currentTrack]
获取的,它会出现为 nil
的情况。
再看 注释2
:
NSMutableDictionary *info = [NSMutableDictionary dictionary];
info[kXMPlayerTrack] = [self currentTrack];
这个字典也使用了 [self currentTrack]
为什么这个地方没有崩溃?
setValue:forKey:
setValue:forKey:
是协议 NSKeyValueCoding
的方法,NSDictionary
也遵守这个协议,并提供了相应的实现。
注释2
那里为 NSDictionary
的属性赋值使用的方式是 setValue:forKey:
,该方法的文档说明如下:
This method adds `value` and `key` to the dictionary using
`setObject(_:forKey:)`, unless `value` is `nil`
in which case the method instead attempts to remove `key` using
`removeObject(forKey:)`.
当某个 value
为 nil
时,NSDictionary
执行的是removeObject(forKey:)
这个方法
因此 注释2
处,不会出现搜集到的那种崩溃!
反过来看看代理方法 playeFailedWithError:track:
,这里面的参数 body
是个 NSDictionary
,是通过 @{,}
的形式生成的。
通过这种形式生成的字典,底层是通过 [__NSPlaceholderDictionary initWithObjects:forKeys:count:]
实现的。其内部会走 setObject:forKey:
setObject:forKey:
文档有这么一句话:
Raises an `NSInvalidArgumentException` if `anObject` is `nil`.
If you need to represent a `nil` value in the dictionary, use `NSNull`.
setObject:forKey:
不接受 nil
值,传过来nil
会抛出异常,但接受 NSNull
类型的值。
我生成 body
参数时,由于 track
的值会为 nil
,这就导致了崩溃。
总结
- 为
NSDictionary
设置属性时,如果该属性的值不确定是否为nil
,那么请使用setValue:_forKey_
进行赋值 - 如果在初始化
NSDictionary
时就赋值,请使用dictionaryWithObjectsAndKeys:
,里面允许使用nil
,但这也意味着赋值的结束,nil
之后的属性值会被忽略掉 - 使用
setValue:_forKey:_
会影响一点效率,因为它会进行-set<Key>:
或_<key>, _is<Key>, <key> 或者 is<Key>
,遍历查找key
。但相对来说比较安全