最近做一个时钟功能,需要精确显示系统时间,记录一下实现的方法。
看似有效的方法
最容易想到的方法就是使用 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 提供的时间就可以根据前面整点显示的思路做更精确的修正。但是要注意一点,时间越精确,系统功耗越高,需要采取一个平衡的策略。