如何精确显示系统时间

1,151 阅读3分钟

最近做一个时钟功能,需要精确显示系统时间,记录一下实现的方法。

看似有效的方法

最容易想到的方法就是使用 Date() 方法,并通过时间格式获取其中的时分秒:

这个方法用于静态展示时间是没有问题的,但是作为时钟展示是有问题的。

func getCurrentTime() -> (hour: String, minute: String, second: String) {
  let now = Date()
  let dateFormatter = DateFormatter()
  dateFormatter.dateFormat = "HH-mm-ss"
  let timeUnits = dateFormatter.string(from: now).split(separator: "-")
  let hour = String(timeUnits[0])
  let minute = String(timeUnits[1])
  let second = String(timeUnits[2])
  return (hour, minute, second)
}

例如通过该方法获取的时间为:18:33:50 ,实际上这个时间还有毫秒、微秒等部分。而时钟展示是需要按整点的,比如到整点 50 秒时再跳动到 50。这个方法解决不了如何正好在整点时分秒时获取到时间并展示。

整点显示

整点获取到时间要解决的关键问题是:让获取时间的代码在整点执行,那么怎么才能做到这一点呢。这个时候就要用到定时器了。

Dispatch 框架提供了一套易用且比较精确的定时器方法: DispatchSourceTimer.按照下面的思路操作:

  • 首先改造 getCurrentTime 方法,让它能获取到毫秒。
  • 通过 Date() 获取一个初始的时间: initialTime
  • 计算这个时间与下一个要显示的整点的时间差: startInterval
  • 利用 DispatchSourceTimer 安排一个 startInterval 之后执行的定时器,定时器第一次触发时即为整点,此时再获取当前时间。
  • 给定时器设定重复间隔时间即可不断获取整点时间。

示例代码如下:

import Dispatch

func getCurrentTime() -> (hour: String, minute: String, second: String, millisecond: String) {
  let now = Date()
  let dateFormatter = DateFormatter()
  dateFormatter.dateFormat = "HH-mm-ss-SSS"
  let timeUnits = dateFormatter.string(from: now).split(separator: "-")
  let hour = String(timeUnits[0])
  let minute = String(timeUnits[1])
  let second = String(timeUnits[2])
  let millisecond = String(timeUnits[3])
  let result = (hour, minute, second, millisecond)
  return result
}

func startTimer() {
  let now = getCurrentTime()
  let startInterval = (1000 - Double(now.millisecond)!) / 1000.0
  let refreshInterval = 1.0
  let timer = DispatchSource.makeTimerSource(flags: [.strict], queue: DispatchQueue.global())
  timer.schedule(deadline: .now() + startInterval, repeating: refreshInterval)
  timer.setEventHandler {
    // ... 定时任务
  }
  timer.resume()
}

提高精度

Date 类最高可以精确到微秒,通过 timeIntervalSince1970 属性可以获取,计算时需要注意精度的问题,尽量使用 Int 型计算,避免使用 Double 做减法(会存在精度丢失的情况):

func getCurrentTimeDownToMicrosecond() -> (hour: String,
                                           minute: String,
                                           second: String,
                                           millisecond: String,
                                           microsecond: String) {
  let now = Date()
  let timeIntervalSince1970 = now.timeIntervalSince1970 // 精确到微秒,如: 1606373453.2862902,注意这是 GMT 时间。
  // 获取今天的总秒数(不含毫秒部分)
  let totalSecondsTodayGMT = Int(timeIntervalSince1970) % (24*60*60)
  // 获取今天的总秒数(毫秒部分)
  let totalSubsecondsToday = Double("0." + String(timeIntervalSince1970).components(separatedBy: ".")[1])!
  
  let hourGMT = totalSecondsTodayGMT / 3600
  // 加上当前时区与 GMT 时区的时间间隔,注意不要超过 24 小时。
  let hourLocal = (hourGMT + TimeZone.current.secondsFromGMT() / 3600) % 24
  let minute = (totalSecondsTodayGMT - hourGMT * 3600) / 60
  let second = totalSecondsTodayGMT - hourGMT * 3600 - minute * 60
  let millisecond = Int(totalSubsecondsToday * 1000)
  let microsecond = Int(totalSubsecondsToday * 1_000_000) - millisecond * 1000
  let result = (String(hourLocal), String(minute), String(second), String(millisecond), String(microsecond))
  return result
}

再精确一点

虽然现在可以获取微秒级别,但是 Date 类还是不够精确,Date 对象在实例化的过程中要额外消耗时间。要想再精确一点,就要用到系统底层的 C 方法了。

有两个方法可以使用:

  • gettimeofday: 表示从 1970-1-1 到现在的秒数,精确到微秒。
  • CFAbsoluteTimeGetCurrent: 表示从 2001-1-1 到现在的秒数,精确到微秒。

其中 gettimeofday 方法更精确一点:

var timeOfDay = timeval()
gettimeofday(&timeOfDay, nil)
print(timeOfDay)
// timeval(tv_sec: 1606397308, tv_usec: 940492)

基于 gettimeofday 提供的时间就可以根据前面整点显示的思路做更精确的修正。但是要注意一点,时间越精确,系统功耗越高,需要采取一个平衡的策略。

参考资料

Precision Timing in iOS & Swift