获取iOS系统的正常运行时间,即使在睡眠时也不会暂停

50

我正在寻找一种获取iOS系统绝对、始终递增的运行时间的方法。

该方法应返回设备上次启动以来的时间,不受系统日期更改的影响。

我能找到的所有方法都会在设备睡眠时暂停(CACurrentMediaTime, [NSProcessInfo systemUptime], mach_absolute_time)或者在系统日期更改时发生变化(sysctl/KERN_BOOTTIME)。

有什么想法吗?

6个回答

54

我想我已经弄清楚了。

time() 在设备休眠时继续递增,但当然可以被操作系统或用户操纵。然而,内核启动时间(系统上次启动的时间戳)在系统时钟改变时也会改变,因此尽管这两个值都不是固定的,它们之间的偏移量是固定的。

#include <sys/sysctl.h>

- (time_t)uptime
{
    struct timeval boottime;
    int mib[2] = {CTL_KERN, KERN_BOOTTIME};
    size_t size = sizeof(boottime);
    time_t now;
    time_t uptime = -1;

    (void)time(&now);

    if (sysctl(mib, 2, &boottime, &size, NULL, 0) != -1 && boottime.tv_sec != 0) {
        uptime = now - boottime.tv_sec;
    }

    return uptime;
}

1
干得好,伙计。在这之前我得去比较服务器日期/本地日期偏移量,现在不用了…非常感谢。 - Magoo
如果在系统时间更新之后但引导时间尚未更新的确切时刻调用此函数,是否存在竞态条件?或者是否有某些保证它们会同时原子更新? - Jon Colverson
1
我不确定。这个应用已经在生产环境中使用了几年,我从未遇到过竞争条件问题。因此,就我的经验而言,它似乎是可以的。 - Russell Quinn
4
是的,存在竞态条件问题。"从未遇到过竞态条件问题"这种论断并不成立,因为:
  1. 您的应用程序可能实际上并不依赖于时钟的单调性。
  2. 您可能没有真正捕捉到现场的这种问题的机制(即您的用户会遇到该问题,但您不知道)。
  3. 您没有大量的用户安装基础,或者您的用户安装基础不能代表其他用户(例如,您的用户从不更改其手动时钟)。
  4. 您的用户可能只是因为幸运而从未遇到此类问题。
- Vitali

30

正如其中一条评论所说,POSIXclock_gettime()已经在iOS 10和macOS 10.12中实现。当使用CLOCK_MONOTONIC参数时,它似乎确实返回了系统运行时间。然而,文档并未保证这一点:

对于该时钟,由 clock_gettime() 返回的值表示自过去某个不确定时间点以来(例如,系统启动时间或纪元),经过的时间(以秒和纳秒为单位)。此点在系统启动时间后不会改变。

以下是从相应的macOS手册中摘录的内容:

CLOCK_MONOTONIC:以单调递增的方式跟踪从任意点开始计时的时间,系统休眠时仍将继续递增。

CLOCK_MONOTONIC_RAW:与CLOCK_MONOTONIC类似,以单调递增的方式跟踪从任意点开始计时的时间。但是,该时钟不受频率或时间调整的影响。不应将其与其他系统时间源进行比较。

CLOCK_MONOTONIC_RAW_APPROX:类似于CLOCK_MONOTONIC_RAW,但读取系统在上下文切换时缓存的值。这样可以更快地读取,但会损失精度,因为它可能返回几毫秒之前的值。

CLOCK_UPTIME_RAW:与CLOCK_MONOTONIC_RAW类似,以单调递增的方式计算时间,但在系统休眠时不会递增。返回值与应用适当的mach_timebase转换后的mach_absolute_time()结果相同。

请注意,CLOCK_MONOTONIC返回微秒精度,而CLOCK_MONOTONIC_RAW返回纳秒精度。

Swift代码:

func uptime() -> timespec {

    var uptime = timespec()
    if 0 != clock_gettime(CLOCK_MONOTONIC_RAW, &uptime) {
        fatalError("Could not execute clock_gettime, errno: \(errno)")
    }

    return uptime
}

print(uptime()) // timespec(tv_sec: 636705, tv_nsec: 750700397)

对于那些仍然对在Swift中获取内核引导时间感兴趣的人:

func kernelBootTime() -> timeval {

    var mib = [ CTL_KERN, KERN_BOOTTIME ]
    var bootTime = timeval()
    var bootTimeSize = MemoryLayout<timeval>.size

    if 0 != sysctl(&mib, UInt32(mib.count), &bootTime, &bootTimeSize, nil, 0) {
        fatalError("Could not get boot time, errno: \(errno)")
    }

    return bootTime
}

print(kernelBootTime()) // timeval(tv_sec: 1499259206, tv_usec: 122778)

为什么 CLOCK_MONOTONIC_RAW 会放弃时间?文档说明它是从一个未指定的任意时间点开始的。 - Gregory Pakosz
我在这里看到了文档:https://www.unix.com/man-page/mojave/3/clock_getres/。它似乎是针对macOS Mojave的。 - William Grand

16

所有示例中存在竞争条件:如果时间更改(NTP或用户修改),则代码将冲突,时钟不再单调。

正确的实现是:

#include <sys/sysctl.h>

static int64_t us_since_boot() {
    struct timeval boottime;
    int mib[2] = {CTL_KERN, KERN_BOOTTIME};
    size_t size = sizeof(boottime);
    int rc = sysctl(mib, 2, &boottime, &size, NULL, 0);
    if (rc != 0) {
      return 0;
    }
    return (int64_t)boottime.tv_sec * 1000000 + (int64_t)boottime.tv_usec;
}

- (int64_t)us_uptime
{
    int64_t before_now;
    int64_t after_now;
    struct timeval now;

    after_now = us_since_boot();
    do {
        before_now = after_now;
        gettimeofday(&now, NULL);
        after_now = us_since_boot();
    } while (after_now != before_now);

    return (int64_t)now.tv_sec * 1000000 + (int64_t)now.tv_usec - before_now;
}

4
我已经测试过了,看起来像个完美的解决方法。我找不出任何问题。谢谢!值得一提的是,现在终于有一个不尴尬的解决方案适用于OS10及以上版本。 clock_gettime API 终于可用,使用 CLOCK_MONOTONIC 应该支持单调计时器的要求,其以墙钟速率增长并具有相当准确性。 - Klaas van Aarsen
1
这很棒,除了函数名称 us_since_boot。它只是启动时间,而不是自启动以来的经过时间。 - Keewon Seo
请注意,在执行 boottime.tv_sec * 1000000now.tv_sec * 1000000 时可能会发生溢出(在32位平台上很容易捕获)。为了避免这个问题,您必须进行直接转换。 - d12frosted
@KeewonSeo 你是对的。最好称其为boot_timestamp()。 - Vitali
1
@IlikeSerena 你说得对。从性能角度来看,CLOCK_MONOTONIC_RAW_APPROX可能是更好的API,如果需要绝对准确的时间戳,则使用CLOCK_MONOTONIC_RAW(我使用高精度时间戳工作,我认为很难找到一个场景,其中RAW是更好的选择)。听起来CLOCK_MONOTONIC仍然可能受到时间/频率调整的影响。 - Vitali

4
如果需要更高的精度,则以下是已接受答案的修改版本。
#include <sys/sysctl.h>

- (NSTimeInterval)uptime
{
    struct timeval boottime;
    int mib[2] = {CTL_KERN, KERN_BOOTTIME};
    size_t size = sizeof(boottime);

    struct timeval now;
    struct timezone tz;
    gettimeofday(&now, &tz);

    double uptime = -1;

    if (sysctl(mib, 2, &boottime, &size, NULL, 0) != -1 && boottime.tv_sec != 0)
    {
        uptime = now.tv_sec - boottime.tv_sec;
        uptime += (double)(now.tv_usec - boottime.tv_usec) / 1000000.0;
    }
    return uptime;
}

2

我想评论Leszek的回答,但我的声望不够... Leszek的解决方案返回秒。

以下是Leszek答案的修改版本,如果有人需要毫秒级精度。

#include <sys/sysctl.h>

+ (long long int)uptime 
{
    struct timeval boottime;
    int mib[2] = {CTL_KERN, KERN_BOOTTIME};
    size_t size = sizeof(boottime);

    struct timeval now;
    struct timezone tz;
    gettimeofday(&now, &tz);

    long long int uptime = -1;

    if (sysctl(mib, 2, &boottime, &size, NULL, 0) != -1 && boottime.tv_sec != 0)
    {
        uptime = ((long long int)(now.tv_sec - boottime.tv_sec)) * 1000;
        uptime += (now.tv_usec - boottime.tv_usec) / 1000;
    }
    return uptime;
}

-6

为什么不使用systemUptime呢?

systemUptime 返回自计算机重新启动以来的时间。

  • (NSTimeInterval)systemUptime 返回值 一个NSTimeInterval,指示自计算机重新启动以来的时间。

可用性 在iOS 4.0及更高版本中可用。 声明于 NSProcessInfo.h

我已经测试并证明,在iPhone 5上安装iOS 7.1.1时,当您的手机被锁定时,systemUptime并不会停止。所以任何人都可以亲自测试一下。


在我的iOS 7.1.x测试中,当设备被锁定并且屏幕关闭时,它不会停止。 - Owen Zhao
它受到睡眠/唤醒的影响,可能是因为你测试时连接了防止睡眠的电缆,或者其他原因阻止了设备进入睡眠状态。 - Maxim Kholyavkin
是的,我重新测试了一下,它停止了,抱歉。 - Russell Quinn
4
单调时间基本上是一个计数器,当物理定时器通过定时器中断向CPU发信号时会递增。几个函数包括将其转换为秒:- [NSProcessInfo systemUptime]、CACurrentMediaTime()等等。然而,由于CPU递增此计数器,因此当CPU关闭电源(包括系统处于“睡眠”状态时)时,单调时钟会停止。 - Russell Quinn
@Speakus非常感谢!你说得对,系统运行时间是由于拔掉电缆而重新启动的!我一整天都在苦苦挣扎,却没有注意到这一点... - Marta Tenés

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接