前言
时间是一个较为抽象的概念,格林威治时间、世界时、祖鲁时间、GMT、UTC、跨时区、夏令时等等关于时间的定义、概念五花八门。但是,我们在编程语言里,认为时间是线性的、不可逆的,某一个时刻,只有一个绝对值,描述时间的时候,描述时间的时候都是以一个时间线上的绝对值为参考点,参考点再加上偏移量(以秒或者毫秒,微秒,纳秒为单位)来描述另外的时间点。
在日常开发的过程中,我们总会在各种场景中跟时间打交道,比如:获取当前时间、距离某个时间的倒计时、方法耗时统计等。伴随而来的问题也就产生了,如何保证获取的是准确时间?一些限时操作,如何与服务器时间同步?怎么防止用户修改时间作弊?获取时间的方法性能如何? 近期在解决某个限时操作的场景中的问题,发现获取时间的方式上很有必要总结一下。
标准时间
GMT(Greenwich Mean Time)
格林威治时间。它规定太阳每天经过位于英国伦敦郊区的皇家格林威治天文台的时间为中午12点。
GMT发展史:
格林威治皇家天文台为了海上霸权的扩张计划,在十七世纪就开始进行天体观测。为了天文观测,选择了穿过英国伦敦格林威治天文台子午仪中心的一条经线作为零度参考线,这条线,简称格林威治子午线。1884年10月在美国华盛顿召开了一个国际子午线会议,该会议将格林威治子午线设定为本初子午线,并将格林威治平时 (GMT, Greenwich Mean Time) 作为世界时间标准(UT, Universal Time)。由此也确定了全球24小时自然时区的划分,所有时区都以和 GMT 之间的偏移量做为参考。1972年之前,格林威治时间(GMT)一直是世界时间的标准。1972年之后,GMT 不再是一个时间标准了。
UTC(Coodinated Universal Time)
协调世界时,又称世界统一时间、世界标准时间、国际协调时间。由于英文(CUT)和法文(TUC)的缩写不同,作为妥协,简称UTC。UTC 是现在全球通用的时间标准,全球各地都同意将各自的时间进行同步协调。UTC 时间是经过平均太阳时(以格林威治时间GMT为准)、地轴运动修正后的新时标以及以秒为单位的国际原子时所综合精算而成。
UTC 由两部分构成:
1、原子时间(TAI, International Atomic Time):结合了全球400个所有的原子钟而得到的时间,它决定了我们每个人的钟表中,时间流动的速度,原子钟50亿年才会误差1秒。
2、世界时间(UT, Universal Time):也称天文时间,或太阳时,他的依据是地球的自转,我们用它来确定多少原子时,对应于一个地球日的时间长度。UTC的历史
1960年,国际无线电咨询委员会规范统一了 UTC 的概念,并在次年投入实际使用。“Coordinated Universal Time”这个名字则在1967年才被正式采纳。1967年以前, UTC被数次调整过,原因是要使用闰秒(leap second)来将 UTC 与地球自转时间进行统一。
我们可以认为格林威治时间就是世界协调时间(GMT=UTC),格林威治时间和UTC时间均用秒数来计算的。
iOS中获取时间的方式
一、NSDate
NSDate objects encapsulate a single point in time, independent of any particular calendrical system or time zone.
Date objects are immutable, representing an invariant time interval relative to an absolute reference date (00:00:00 UTC on 1 January 2001).
我们平时调用NSFoundation框架下的获取时间方式[NSDate date]
获取当前时间就是UTC时间,是以为参考的时间间隔。但是我们在与服务器的交互中,服务器通常使用的是Unix time,而Unix time是以UTC 为参考的时间间隔。NSDate提供了这种转换的能力,看下NSDate的API:
@property (readonly) NSTimeInterval timeIntervalSinceNow; //以当前时间为基准
@property (readonly) NSTimeInterval timeIntervalSince1970;//以1970年1月1号 00:00:00为基准
@property (readonly) NSTimeInterval timeIntervalSinceReferenceDate;//以2001年1月1日00:00:00为基准
实例代码:
NSDate* date = [NSDate date];
NSLog(@"%@",date);
NSLog(@"timeIntervalSinceNow: %f", [date timeIntervalSinceNow]);
NSLog(@"timeIntervalSince1970: %f", [date timeIntervalSince1970]);
NSLog(@"timeIntervalSinceReferenceDate: %f", [date timeIntervalSinceReferenceDate]);
输出结果:
2020-12-12 20:16:58.160637+0800 DateTest[6402:2425204] Sat Dec 12 20:16:58 2020
2020-12-12 20:16:58.160686+0800 DateTest[6402:2425204] timeIntervalSinceNow: -0.000064
2020-12-12 20:16:58.160717+0800 DateTest[6402:2425204] timeIntervalSince1970: 1607775418.160621
2020-12-12 20:16:58.160739+0800 DateTest[6402:2425204] timeIntervalSinceReferenceDate: 629468218.160621629467211.468034
注意2020-12-12 20:16:58.160637+0800
输出的时间是+8小时的,因为北京属于UTC+8的时区,那么UTC的绝对时间就是2020-12-12 12:16:58.160637+0800
。不同时区显示的时间不一致,但是绝对时间的偏移量是相同的,NSDate具体要展示成什么样,取决于NSDateFormatter
和NSTimeZone
。
最重要一点:NSDate获取的时间,受系统控制,用户可以修改
。
二、CFAbsoluteTimeGetCurrent
Absolute time is measured in seconds relative to the absolute reference date of Jan 1 2001 00:00:00 GMT. A positive value represents a date after the reference date, a negative value represents a date before it. For example, the absolute time -32940326 is equivalent to December 16th, 1999 at 17:54:34. Repeated calls to this function do not guarantee monotonically increasing results. The system time may decrease due to synchronization with external time references or due to an explicit user change of the clock.
Core Foundation框架下获取当前时间的方式,以为时间基准。
NSDate* date = [NSDate date];
CFAbsoluteTime absoluteTime = CFAbsoluteTimeGetCurrent();
NSLog(@"NSDate timeIntervalSinceReferenceDate: %f", [date timeIntervalSinceReferenceDate]);
NSLog(@"CFAbsoluteTime: %f", absoluteTime);
2020-12-12 20:38:19.120249+0800 DateTest[6407:2430605] timeIntervalSinceReferenceDate: 629469499.120139
2020-12-12 20:38:19.120267+0800 DateTest[6407:2430605] CFAbsoluteTime: 629469499.120266
输出结果可以看出,与NSDate 的timeIntervalSinceReferenceDate获取的时间戳一致。同样受系统控制,用户可以修改
。
三、gettimeofday函数
C函数获取当前时间int gettimeofday(struct timeval * __restrict, void * __restrict);
2020-12-12 20:59:29.681949+0800 DateTest[6472:2441874] CFAbsoluteTime: 629470769.681292
2020-12-12 20:59:29.682059+0800 DateTest[6472:2441874] getTimeOfDay: 1607777969.000000
以UTC 为基准,同样受系统控制,用户可以修改
。
四、mach_absolute_time函数
Returns current value of a clock that increments monotonically in tick units (starting at an arbitrary point), this clock does not increment while the system is asleep.
uint64_t nStartTick = mach_absolute_time()
NSLog(@"nStartTick: %llu",nStartTick);
2020-12-12 22:10:30.002634+0800 DateTest[311:4891] nStartTick: 2188669381
uint64_t mach_absolute_time(void);
该函数返回的是CPU已经运行的“滴答”数,手机重启后,重新从一个任意值开始计数,不受系统控制,只受设备重启和休眠行为影响
这个“滴答”数可以通过mach_timebase_info转换成秒,即重启后运行了多久,方法如下:
double machTime = (double)mach_absolute_time()*(double)timebase.numer/(double)timebase.denom/1e9;//转换为 s
NSLog(@"nStartTick to s: %f",machTime);
2020-12-12 22:18:40.930518+0800 DateTest[359:8022] nStartTick to s: 592.234748
五、CACurrentMediaTime
Returns the current CoreAnimation absolute time. This is the result of calling mach_absolute_time () and converting the units to seconds.
看文档是不是很熟悉,这个时间就是** mach_absolute_time**方式获取的CPU启动后的运行时间,这个得到的直接是以秒为单位,不受系统控制,只受设备重启和休眠行为影响
2020-12-12 22:29:52.069621+0800 DateTest[368:10795] nStartTick to s: 1263.375108
2020-12-12 22:29:52.069740+0800 DateTest[368:10795] CACurrentMediaTime :1263.375350
六、NSProcessInfo processInfo
The amount of time the system has been awake since the last time it was restarted.
通过NSProcessInfo获取系统重启后的运行时间不受系统控制,只受设备重启和休眠行为影响
NSLog(@"CACurrentMediaTime :%f",CACurrentMediaTime());
NSLog(@"NSProcessInfo :%f", [[NSProcessInfo processInfo] systemUptime]);
2020-12-12 22:37:25.380199+0800 DateTest[372:12605] CACurrentMediaTime :1716.685811
2020-12-12 22:37:25.380374+0800 DateTest[372:12605] NSProcessInfo :1716.685986
七、sysctl函数
int sysctl(int *, u_int, void *, size_t *, void *, size_t);
获取设备上次重启的Unix time,受系统控制,用户可以修改
。
调用方式如下:
- (long)_bootTime {
struct timeval bootTime;
int mib[2] = {CTL_KERN,KERN_BOOTTIME};
size_t size = sizeof(bootTime);
if (sysctl(mib, 2, &bootTime, &size, NULL, 0) != -1) {
return bootTime.tv_sec;///秒
// return bootTime.tv_usec;///微秒
};
return 0;
}
八、clock_gettime函数
int clock_gettime(clockid_t __clock_id, struct timespec *__tp);
**clockid_t**的常用枚举值:
CLOCK_REALTIME, a system-wide realtime clock.
CLOCK_PROCESS_CPUTIME_ID, high-resolution timer provided by the CPUfor each process.
CLOCK_THREAD_CPUTIME_ID, high-resolution timer provided by the CPUfor each of the threads.
CLOCK_REALTIME,a system-wide realtime clock.
CLOCK_PROCESS_CPUTIME_ID, high-resolution timer providedby the CPU for each process.
CLOCK_THREAD_CPUTIME_ID, high-resolution timer provided bythe CPU for each of the threads.
这个函数只iOS10以上的系统可用。调用方式如下:
- (long)_getSystemUptime {
struct timespec ts;
if (@available(iOS 10.0, *)) {
clock_gettime(CLOCK_REALTIME, &ts);
} else {
}
return ts.tv_sec;
// return ts.tv_nsec;//纳秒
}
//输出
DateTest[507:34153] getTimeOfDay: 1607846040.000000
DateTest[507:34153] Clock_realtimeUptime: 1607846040.000000
CLOCK_REALTIME 代表机器上的实际时间,与gettimeofday()函数获取的时间一样,以UTC 为基准,受系统控制,用户可以修改
clock_gettime()可以得到跟gettimeofday()函数一样的当前时间,但是更加精准,可以到纳秒级的精度。
对比下两个函数的参数的结构体:struct timeval
,struct timespec
_STRUCT_TIMEVAL
{
__darwin_time_t tv_sec; /* seconds */
__darwin_suseconds_t tv_usec; /* and microseconds */
};
_STRUCT_TIMESPEC
{
__darwin_time_t tv_sec;
long tv_nsec;
};
同步服务器时间
在我们的考试的业务场景中,时间是一个非常关键的参数,限时的考试任务,考试可持续的时长、考试开始时间、结束时间的判断,都需要我们和服务器时间保持一致。
我们现有的做法,是在App启动的时候,同步一下服务器时间,拿到服务器给的时间serverTime,和本地时间localTime计算出一个差值diff,保存起来。下次调用取当前时间currentTime的时候,再次取localTime加上这个diff得出真正的时间。
那么localTime又如何取值呢?上面介绍的NSDate、CFAbsoluteTimeGetCurrent()、gettimeofday()、sysctl()都受系统时间影响,mach_absolute_time()、CACurrentMediaTime()、NSProcessInfo获取的CPU“滴答”数又受设备重启、休眠的影响。
参考其他客户端实现方案,我们使用函数sysctl()获取系统上次重启的时间,在获取本地当前时间gettimeofday(),二者都受系统时间影响,但是两者相减,就跟系统时间没有关系了。
代码实现:
- (NSTimeInterval)_systemUptime {
// 获取系统上次重启时间
struct timeval bootTime;
int mib[2] = {CTL_KERN, KERN_BOOTTIME};
size_t size = sizeof(bootTime);
int resutl = sysctl(mib, 2, &bootTime, &size, NULL, 0);
// 获取当前时间
struct timeval now;
struct timezone tz;
gettimeofday(&now, &tz);
NSTimeInterval uptime = -1;
if (resutl != -1 && bootTime.tv_sec != 0) {
uptime = now.tv_sec - bootTime.tv_sec;
}
return uptime;
}
第一次同步服务器时间的时候计算出差值timeDiff
:
double interval = [[[NSString alloc]initWithData:jsonData encoding:kCFStringEncodingUTF8] doubleValue];
NSDate *serverDate = [NSDate dateWithTimeIntervalSince1970:interval / 1000.0];
//获取到与系统时间无关的upTime
NSTimeInterval uptime = [self _systemUptime];
NSDate *cpuDate = [NSDate dateWithTimeIntervalSince1970:uptime];
self.timeDiff = [[NSDate dateWithTimeIntervalSince1970:uptime] timeIntervalSinceDate:serverDate];
后边再取当前时间的时候,就用upTime加上TimeDiff:
NSDate *cpuDate = [NSDate dateWithTimeIntervalSince1970: [self _systemUptime]];
NSDate *realCurrentDate = [cpuDate dateByAddingTimeInterval:-self.timeDiff];
对比下修改时间前后的输出结果,把当前时间修改到20点,diff后得到的时间依然准确:
2020-12-13 16:47:28.830748+0800 DateTest[646:68713] ServerDate :Sun Dec 13 16:47:28 2020 --:1607849248.976000
2020-12-13 16:47:28.830919+0800 DateTest[646:68713] LocalDate :Sun Dec 13 16:47:28 2020 --:1607849248.826418
2020-12-13 16:47:28.831037+0800 DateTest[646:68713] after diff Date :Sun Dec 13 16:47:28 2020 --:1607849248.976000
2020-12-13 20:48:03.310993+0800 DateTest[646:68713] 修改时间
2020-12-13 20:48:03.311147+0800 DateTest[646:68713] LocalDate :Sun Dec 13 20:48:03 2020 --:1607863683.309711
2020-12-13 20:48:03.311223+0800 DateTest[646:68713] after diff Date :Sun Dec 13 16:48:14 2020 --:1607849294.976000
总结
时间问题看似很小,但是出现问题,又很难跟踪、排查,你不知道用户做了哪些操作,影响了时间,对于时间要求比较严格的场景,要尽可能多的和服务器时间同步,我们需要做的就是尽可能的减少这种影响,从而避免影响程序的逻辑。
以上就是本次对iOS获取时间问题的总结,如有问题,欢迎指正。