美文网首页
setObject:forKey: vs setValueFor

setObject:forKey: vs setValueFor

作者: 厨子 | 来源:发表于2018-10-22 18:45 被阅读28次

    背景

    在音频播放的项目中有个需求:监听到播放失败,通过 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<Key>:_<key>, _is<Key>, <key> 或者 is<Key>,遍历查找 key。但相对来说比较安全

    相关文章

      网友评论

          本文标题:setObject:forKey: vs setValueFor

          本文链接:https://www.haomeiwen.com/subject/nohgzftx.html