【问题标题】:Why NSDate's dateWithTimeIntervalSince1970 modifies its input?为什么 NSDate 的 dateWithTimeIntervalSince1970 会修改其输入?
【发布时间】:2018-03-25 19:56:08
【问题描述】:

我正在运行以下代码:

for (int i = 0; i < 100; ++i) {
  NSDate *date = [NSDate date];
  NSTimeInterval interval = date.timeIntervalSince1970;
  NSDate *newDate = [NSDate dateWithTimeIntervalSince1970:interval];

  if (![date isEqualToDate:newDate]) {
    NSLog(@"Not equal!");
  }
}

令人惊讶的是,在许多迭代中,日期并不相等。怎么可能?

【问题讨论】:

    标签: ios objective-c nsdate


    【解决方案1】:

    反汇编dateWithTimeIntervalSince1970:显示调用-initWithIntervalSinceReferenceDate:

    CoreFoundation`+[NSDate dateWithTimeIntervalSince1970:]:
        0x10fd39430 <+0>:  pushq  %rbp
        0x10fd39431 <+1>:  movq   %rsp, %rbp
        0x10fd39434 <+4>:  pushq  %rbx
        0x10fd39435 <+5>:  pushq  %rax
        0x10fd39436 <+6>:  movsd  %xmm0, -0x10(%rbp)
        0x10fd3943b <+11>: movq   0x2d8146(%rip), %rsi      ; "alloc"
        0x10fd39442 <+18>: movq   0x29edc7(%rip), %rbx      ; (void *)0x000000010f35e940: objc_msgSend
        0x10fd39449 <+25>: callq  *%rbx
        0x10fd3944b <+27>: movsd  -0x10(%rbp), %xmm0        ; xmm0 = mem[0],zero 
        0x10fd39450 <+32>: addsd  0x1f9d58(%rip), %xmm0     ; _CFLog_os_trace_type_map + 16
        0x10fd39458 <+40>: movq   0x2d9041(%rip), %rsi      ; "initWithTimeIntervalSinceReferenceDate:"
        0x10fd3945f <+47>: movq   %rax, %rdi
        0x10fd39462 <+50>: callq  *%rbx
        0x10fd39464 <+52>: movq   0x2d81b5(%rip), %rsi      ; "autorelease"
        0x10fd3946b <+59>: movq   %rax, %rdi
        0x10fd3946e <+62>: movq   %rbx, %rax
        0x10fd39471 <+65>: addq   $0x8, %rsp
        0x10fd39475 <+69>: popq   %rbx
        0x10fd39476 <+70>: popq   %rbp
        0x10fd39477 <+71>: jmpq   *%rax
        0x10fd39479 <+73>: nopl   (%rax)
    

    此外,初始化程序的输入是secs - 978307200(或secs - NSTimeIntervalSince1970),这是1970 年与参考日期之间的时间差。这种计算虽然从双精度数中减去整数(双精度数),但由于舍入误差,可能会更改输入值的分数。例如,这是一个未通过测试的日期:

    date: Sun Mar 25 17:54:39 2018 (543682479.8504179716),
    newDate: Sun Mar 25 17:54:39 2018 (543682479.8504180908)
    

    由于log2(543682479.8504179716 + NSTimeIntervalSince1970) ~ 30.5log2(543682479.8504179716) ~ 29.01,双精度值的指数需要调整,尾数需要归一化,可能会影响小数值。

    解决方案是改用+dateWithTimeIntervalSinceReferenceDate:工厂方法,直接初始化NSDate,无需额外计算。

    【讨论】:

    • 你真的不需要反汇编就能看到initWithIntervalSinceReferenceDate: 被发送。这是通常的初始化模式但是,舍入是有意的,而不是意外的舍入失败。
    • 这是正确的答案;在NSDateNSTimeInterval t1 = 543682479.8504179716; NSTimeInterval t2 = (t1 + NSTimeIntervalSince1970) - NSTimeIntervalSince1970; printf("%d", t1 == t2); // =&gt; 0 的参与下,您可以非常容易地看到浮点精度的这种损失:浮点值在这里失去了足够的精度,在精确比较中不再相等。
    • @AminNegm-Awad,虽然initWithIntervalSinceReferenceDate:被标记为指定初始化器,但不看实际代码,不清楚输入时间间隔是如何处理的。 NSDate 的文档中也没有记录这一点。
    • 几年前我遇到过你的问题,经过长时间的研究,我找到了解释,可能在邮件列表中。然而,便利的分配器总是调用其对应的初始化器,即使它不是指定的初始化器。是否被指定的属性仅对从 init 到 init 的方式很重要。
    • 反汇编是您所写内容的一个很好的反例:您希望它调用-initWithTimeIntervalSince1970:(对应的初始化程序),但它直接调用了指定的初始化程序。无论如何,我认为这超出了问题的范围。
    【解决方案2】:

    引用 1970 的方法用于自 1970 年以来的 BSD 样式日期间隔。定义了一个分辨率,IIRC 毫秒。因此,NSTimeValue 值有意四舍五入到该规格。由于这种舍入,您不能期望转换和重新转换的值是相同的值。

    只有在获得 BSD 时才使用这些方法。这是记录在案的:

    此方法对于从 BSD 系统函数返回的 time_t 值创建 NSDate 对象很有用。

    https://developer.apple.com/documentation/foundation/nsdate/1591576-datewithtimeintervalsince1970

    但是,正如 StatusReport 所说,日期是浮点数。测试平等总是危险的。 IE。当您使用 Core Data 存储日期时,它会“四舍五入”到 SQL 列的分辨率。

    【讨论】:

    • 这是不正确的。 NSDate 没有时间分辨率,也没有实际四舍五入的值;正如@StatusReport 所说,问题在于浮点精度的损失。
    • 或者更确切地说,NSTimeInterval 的时间分辨率是 double 中的任何内容。 -initWithTimeIntervalSince1970: 的实现只取给定的时间间隔,减去NSTimeIntervalSince1970(计算自参考日期以来的时间间隔),然后调用-initWithTimeIntervalSinceReferenceDate:;不涉及四舍五入。
    • 这仍然不正确。 -initWithTimeIntervalSince1970: 从给定的时间间隔中减去NSTimeIntervalSince1970 并将结果传递给-initWithTimeIntervalSinceReferenceDate:timeIntervalSince1970 方法从参考日期开始计算时间并添加 NSTimeIntervalSince1970 回来。您可以非常轻松地向自己证明这一点:
    • for (NSTimeInterval i = 0; i
    • 很难在 cmets 中格式化代码,但上面没有打印任何内容。对于每个时间间隔,通过-initWithTimeIntervalSince1970:timeIntervalSince1970 来回返回完全相同的结果与您自己进行加法和减法相同。您可以使用循环的分辨率,它只需要更长的时间。这段代码中没有四舍五入; NSDate 从不修改你给它的值,只是包装它。
    猜你喜欢
    • 1970-01-01
    • 2011-10-27
    • 1970-01-01
    • 2016-07-13
    • 2011-04-27
    • 2020-10-26
    • 2021-08-02
    • 2016-02-18
    相关资源
    最近更新 更多