iOS/macOS 关于时间的处理

转载: http://www.cocoachina.com/ios/20161216/18361.html

做App避免不了要和时间打交道,关于时间的处理,里面有不少门道,远不是一行API调用,获取当前系统时间这么简单。我们需要了解与时间相关的各种API之间的差别,再因场景而异去设计相应的机制。

时间的形式

在开始深入讨论之前,我们需要确信一个前提:时间是线性的。即任意一个时刻,这个地球上只有一个绝对时间值存在,只不过因为时区或者文化的差异,处于同一时空的我们对同一时间的表述或者理解不同。这个看似简单明了的道理,是我们理解各种与时间相关的复杂概念的基石。就像UTF-8和UTF-16其实都是Unicode一样,北京的20:00和东京的21:00其实是同一个绝对的时间值。

GMT

人类对于时间的理解还很有限,但我们至少能确定一点:时间的变化是匀速的。时间前进的速度是均匀的,不会忽快忽慢,所以为了描述时间,我们也需要找到一个值,它的变化也是以均匀的速度向前变化的。

说出来你可能不信,我们人类为了寻找这个参考值,来精确描述当前的时间值,都经历了漫长岁月的探索。你可以尝试思考下,生活中有什么事物是随着时间均匀变化的,它具备的数值属性,会随着时间处于绝对的匀速变化状态。

前人发现抬头看太阳是个好办法,太阳总是按规律的“早起晚落”,而且“亘古不变”,可以用太阳在一天当中所处的位置来描述当前的时间。后来不同地区的文化需要交流,你这里太阳正高空照,我这可能已经下山了,所以需要有一个公共的大家都认可的地方,以这个地方太阳的位置来做参考着,沟通起来就会方便很多。最后选择的是英国伦敦的格林尼治天文台所在地,以格林尼治的时间作为公共时间,也就是我们所说的GMT时间(Greenwich Mean Time)。

UTC

太阳所处的位置变化跟地球的自转相关,过去人们认为地球自转的速率是恒定的,但在1960年这一认知被推翻了,人们发现地球自转的速率正变得越来越慢,而时间前进的速率还是恒定的,所以GMT不再被认为可以用来精准的描述时间了。

我们需要继续寻找一个匀速前进的值。抬头看天是我们从宏观方向去寻找答案,科技的发展让我们在微观方面取得了更深的认识,于是有聪明人根据微观粒子原子的物理属性,建立了原子钟,以这种原子钟来衡量时间的变化,原子钟50亿年才会误差1秒,这种精读已经远胜于GMT了。这个原子钟所反映的时间,也就是我们现在所使用的UTC(Coordinated Universal Time )标准时间。

接下来我们看下iOS里,五花八门的记录时间的方式。

NSDate

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).

NSDate对象描述的是时间线上的一个绝对的值,和时区和文化无关,它参考的值是:以UTC为标准的,2001年一月一日00:00:00这一刻的时间绝对值。

这里有个概念很重要,我们用编程语言描述时间的时候,都是以一个时间线上的绝对值为参考点,参考点再加上偏移量(以秒或者毫秒,微秒,纳秒为单位)来描述另外的时间点。

理解了这一点,再看NSDate的一些API调用就非常清楚了,比如:

NSDate *date = [NSDate date];
NSLog(@"Current date interval: %f",[date timeIntervalSinceReferenceDate]);
//2019-02-20 14:36:26.756224+0800 集合类安全[8547:9732099] Current date interval: 572337386.756210

timeIntervalSinceReferenceDate返回的是距离参考时间的偏移量,这个偏移量的值为572337386.756210秒,572337386.756210/86400/365=18.14869947857084,86400是一天所包含的秒数,365大致是一年的天数,15.94当然就是年数了,算出来刚好是此刻距离2001年的差值。

又比如,此刻我写文章的时候,当前时间为北京时间下午14:43,看看下面代码的输出:

NSDate *date = [NSDate date];
NSLog(@"Current date: %@",date);
//2019-02-20 14:43:48.816694+0800 集合类安全[8587:9735240] Current date: Wed Feb 20 14:43:48 2019

NSDate和市区和文化无关,所以要展示具体格式的时间,我们需要NSDateFormatter和NSTimeZone的辅助。

另外关于NSDate最重要的一点是:NSDate是受手机系统时间控制的。也就是说,当你修改了手机上的时间显示,NSDate获取当前时间的输出也会随之改变。在我们做App的时候,明白这一点,就知道NSDate并不可靠,因为用户可能会修改它的值。

Unix time

Unix time是以UTC 1970年1月1号 00:00:00为基准时间,当前时间距离基准点偏移的秒数。

实际上NSDate也有一个API能返回Unix time:

 [date timeIntervalSince1970];

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.

从上面的描述不难看出CFAbsoluteTimeGetCurrent()的概念和NSDate非常相似,只不过参考点是:以GMT为标准的,2001年一月一日00:00:00这一刻的时间绝对值。

同样CFAbsoluteTimeGetCurrent()也会跟着当前设备的系统时间一起变化,也可能会被用户修改。

mach_absolute_time()

mach_absolute_time()可能用到的同学比较少,但这个概念非常重要。

前面提到我们需要找到一个均匀变化的属性值来描述时间,而在我们的iPhone上刚好有一个这样的值存在,就是CPU的时钟周期数(ticks)。这个tick的数值可以用来描述时间,而mach_absolute_time()返回的就是CPU已经运行的tick的数量。将这个tick数经过一定的转换就可以变成秒数,或者纳秒数,这样就和时间直接关联了。

不过这个tick数,在每次手机重启之后,会重新开始计数,而且iPhone锁屏进入休眠之后tick也会暂停计数。

mach_absolute_time()不会受系统时间影响,只受设备重启和休眠行为影响。

CACurrentMediaTime()

CACurrentMediaTime()可能接触到的同学会多一些,先看下官方定义:

A CFTimeInterval derived by calling mach_absolute_time() and converting the result to seconds.

CACurrentMediaTime()就是将上面mach_absolute_time()的CPU tick数转化成秒数的结果。以下代码:

- (IBAction)testTryCatch:(id)sender {
    double start = CACurrentMediaTime();
    NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithCapacity:0];
    
    for (int i = 0; i<3000000; i++) {

        @try {
            NSString *key = [NSString stringWithFormat:@"key%d",i];
            [dic setValue:key forKey:key];
        } 
        @catch (NSException *exception) {
        } 
        @finally {
        }
    }
    double end = CACurrentMediaTime();
    NSLog(@"Try Catch: %f",end - start);

}

返回的就是开机后设备一共运行了(设备休眠不统计在内)多少秒,另一个API也能返回相同的值:

    NSTimeInterval systemUptme = [[NSProcessInfo processInfo] systemUptime];
   

CACurrentMediaTime()也不会受系统时间影响,只受设备重启和休眠行为影响。

有了以上获取时间的各种手段,我们再来看看一些场景之下的具体应用。

场景一,时间测量

我们做性能优化的时候,经常需要对某个方法执行的时间做记录,就必然会用到上面提到的一些获取时间的方法。

在做方法执行时间的benchmark的时候,我们获取时间的方法要满足两个要求,一是精读要高,而是API本身几乎不耗CPU时间。

客户端做性能优化一般是为了主线程的流畅性,而我们知道UI线程如果遇到超过16.7ms的阻塞,就会出现掉帧现象,所以我们关注的时间的精读实际上是在毫秒(ms)级别。我们写客户端代码的时候,基本上都是处于ms这一维度,如果一个方法损耗是0.1ms,我们可以认为这个方法对于流畅性来说是安全的,如果经常看到超过1ms或者几个ms的方法,主线程出现卡顿的几率就会变高。

上面几种获取时间的方式精读上都是足够的,比如一个NSDateAPI调用返回的精读是0.000004 S,也就是4微秒,CACurrentMediaTime()返回的精读也在微秒级别,精读上都符合要求。不过有一种看法,认为NSDate属于类的封装,OOP高级语言本身所带来的损耗可能会影响最后的实际结果,在做benchmark的时候不如C函数调用精准

场景二:客户端和服务器之间的时间同步

这也是我们经常遇到的场景,比如电商类App到零点的时候开始抢购,比如商品限购倒计时等等,这种场景下需要我们将客户端的时间与服务器保持一致,最重要的是,要防止用户通过断网修改系统时间,来影响客户端的逻辑。

比较普遍的做法是,在一些常用的Server接口里面带上服务器时间,每调用一次接口,客户端就和服务器时间做一次同步并记录下来,但问题是如何防止用户修改呢?

上面提到的NSDate,CFAbsoluteTimeGetCurrent都是跟随系统时间变化的,mach_absolute_time和CACurrentMediaTime虽然是依据CPU时钟数,不受系统时间影响,但在休眠和重启的时候还是会被影响。

为了实现以上需求,我们需要:

  • 获取服务器某一时刻 A 的时间;
  • 记录获取到时刻 A 时的本地时间 B;
  • 需要用到时间时,获取当前本地时间 C,当 C - B 作为时间间隔 D,则 A + D 则是当前服务器的时间。

总结

关于时间处理的逻辑就总结到这里了,关键还在于我们对于时间本身的理解,对于表达时间的各种方式的理解,理解背后的原理才能选择合适的工具。

发布了34 篇原创文章 · 获赞 20 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/ZhangWangYang/article/details/87801929