setObject:forKey: vs setValueForKey:

背景

在音频播放的项目中有个需求:监听到播放失败,通过 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:)`.

当某个 valuenil 时,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:_, _is, 或者 is,遍历查找 key。但相对来说比较安全

你可能感兴趣的:(setObject:forKey: vs setValueForKey:)