IOS 开发APP之关于时间处理详细介绍
IOS时间处理
做App避免不了要和时间打交道,关于时间的处理,里面有不少门道,远不是一行API调用,获取当前系统时间这么简单。我们需要了解与时间相关的各种API之间的差别,再因场景而异去设计相应的机制。
时间的形式
在开始深入讨论之前,我们需要确信一个前提:时间是线性的。即任意一个时刻,这个地球上只有一个绝对时间值存在,只不过因为时区或者文化的差异,处于同一时空的我们对同一时间的表述或者理解不同。这个看似简单明了的道理,是我们理解各种与时间相关的复杂概念的基石。就像UTF-8和UTF-16其实都是Unicode一样,北京的20:00和东京的21:00其实是同一个绝对的时间值。
GMT
人类对于时间的理解还很有限,但我们至少能确定一点:时间的变化是匀速的。时间前进的速度是均匀的,不会忽快忽慢,所以为了描述时间,我们也需要找到一个值,它的变化也是以均匀的速度向前变化的。
说出来你可能不信,我们人类为了寻找这个参考值,来精确描述当前的时间值,都经历了漫长岁月的探索。你可以尝试思考下,生活中有什么事物是随着时间均匀变化的,它具备的数值属性,会随着时间处于绝对的匀速变化状态。
前人发现抬头看太阳是个好办法,太阳总是按规律的“早起晚落”,而且“亘古不变”,可以用太阳在一天当中所处的位置来描述当前的时间。后来不同地区的文化需要交流,你这里太阳正高空照,我这可能已经下山了,所以需要有一个公共的大家都认可的地方,以这个地方太阳的位置来做参考着,沟通起来就会方便很多。最后选择的是英国伦敦的格林尼治天文台所在地,以格林尼治的时间作为公共时间,也就是我们所说的GMT时间(GreenwichMeanTime)。
UTC
太阳所处的位置变化跟地球的自转相关,过去人们认为地球自转的速率是恒定的,但在1960年这一认知被推翻了,人们发现地球自转的速率正变得越来越慢,而时间前进的速率还是恒定的,所以UTC不再被认为可以用来精准的描述时间了。
我们需要继续寻找一个匀速前进的值。抬头看天是我们从宏观方向去寻找答案,科技的发展让我们在微观方面取得了更深的认识,于是有聪明人根据微观粒子原子的物理属性,建立了原子钟,以这种原子钟来衡量时间的变化,原子钟50亿年才会误差1秒,这种精读已经远胜于GMT了。这个原子钟所反映的时间,也就是我们现在所使用的UTC(CoordinatedUniversalTime)标准时间。
接下来我们看下iOS里,五花八门的记录时间的方式。
NSDate
NSDate是我们平时使用较多的一个类,先看下它的定义:
NSDateobjectsencapsulateasinglepointintime, independentofanyparticularcalendricalsystemortimezone. Dateobjectsareimmutable,representinganinvarianttimeinterval relativetoanabsolutereferencedate(00:00:00UTCon1January2001).
NSDate对象描述的是时间线上的一个绝对的值,和时区和文化无关,它参考的值是:以UTC为标准的,2001年一月一日00:00:00这一刻的时间绝对值。
这里有个概念很重要,我们用编程语言描述时间的时候,都是以一个时间线上的绝对值为参考点,参考点再加上偏移量(以秒或者毫秒,微秒,纳秒为单位)来描述另外的时间点。
理解了这一点,再看NSDate的一些API调用就非常清楚了,比如:
NSDate*date=[NSDatedate]; NSLog(@"currentdateinterval:%f",[datetimeIntervalSinceReferenceDate]);
timeIntervalSinceReferenceDate返回的是距离参考时间的偏移量,这个偏移量的值为502945767秒,502945767/86400/365=15.9483056507,86400是一天所包含的秒数,365大致是一年的天数,15.94当然就是年数了,算出来刚好是此刻距离2001年的差值。
又比如,此刻我写文章的时候,当前时间为北京时间上午11:29,看看下面代码的输出:
NSDate*date=[NSDatedate]; NSLog(@"currentdate:%@",date);
currentdate:2016-12-0903:29:09+0000。可见NSDate输出的是绝对的UTC时间,而北京时间的时区为UTC+8,上面的输出+8个小时,刚好就是我当前的时间了。
NSDate和市区和文化无关,所以要展示具体格式的时间,我们需要NSDateFormatter和NSTimeZone的辅助。
另外关于NSDate最重要的一点是:NSDate是受手机系统时间控制的。也就是说,当你修改了手机上的时间显示,NSDate获取当前时间的输出也会随之改变。在我们做App的时候,明白这一点,就知道NSDate并不可靠,因为用户可能会修改它的值。
CFAbsoluteTimeGetCurrent()
官方定义如下:
AbsolutetimeismeasuredinsecondsrelativetotheabsolutereferencedateofJan1200100:00:00GMT. Apositivevaluerepresentsadateafterthereferencedate,anegativevaluerepresentsadatebeforeit. Forexample,theabsolutetime-32940326isequivalenttoDecember16th,1999at17:54:34. Repeatedcallstothisfunctiondonotguaranteemonotonicallyincreasingresults. Thesystemtimemaydecreaseduetosynchronizationwithexternaltimereferencesorduetoanexplicit userchangeoftheclock.
从上面的描述不难看出CFAbsoluteTimeGetCurrent()的概念和NSDate非常相似,只不过参考点是:以GMT为标准的,2001年一月一日00:00:00这一刻的时间绝对值。
同样CFAbsoluteTimeGetCurrent()也会跟着当前设备的系统时间一起变化,也可能会被用户修改。
gettimeofday
这个API也能返回一个描述当前时间的值,代码如下:
structtimevalnow; structtimezonetz; gettimeofday(&now,&tz); NSLog(@"gettimeofday:%ld",now.tv_sec);
使用gettimeofday获得的值是Unixtime。Unixtime又是什么呢?
Unixtime是以UTC1970年1月1号00:00:00为基准时间,当前时间距离基准点偏移的秒数。上述API返回的值是1481266031,表示当前时间距离UTC1970年1月1号00:00:00一共过了1481266031秒。
Unixtime也是平时我们使用较多的一个时间标准,在Mac的终端可以通过以下命令转换成可阅读的时间:
date-r1481266031
实际上NSDate也有一个API能返回Unixtime:
NSDate*date=[NSDatedate]; NSLog(@"timeIntervalSince1970:%f",[datetimeIntervalSince1970]);
gettimeofday和NSDate,CFAbsoluteTimeGetCurrent()一样,都是受当前设备的系统时间影响。只不过是参考的时间基准点不一样而已。我们和服务器通讯的时候一般使用Unixtime。
mach_absolute_time()
mach_absolute_time()可能用到的同学比较少,但这个概念非常重要。
前面提到我们需要找到一个均匀变化的属性值来描述时间,而在我们的iPhone上刚好有一个这样的值存在,就是CPU的时钟周期数(ticks)。这个tick的数值可以用来描述时间,而mach_absolute_time()返回的就是CPU已经运行的tick的数量。将这个tick数经过一定的转换就可以变成秒数,或者纳秒数,这样就和时间直接关联了。
不过这个tick数,在每次手机重启之后,会重新开始计数,而且iPhone锁屏进入休眠之后tick也会暂停计数。
mach_absolute_time()不会受系统时间影响,只受设备重启和休眠行为影响。
CACurrentMediaTime()
CACurrentMediaTime()可能接触到的同学会多一些,先看下官方定义:
/*ReturnsthecurrentCoreAnimationabsolutetime.Thisistheresultof *callingmach_absolute_time()andconvertingtheunitstoseconds.*/ CFTimeIntervalCACurrentMediaTime(void)
CACurrentMediaTime()就是将上面mach_absolute_time()的CPUtick数转化成秒数的结果。以下代码:
doublemediaTime=CACurrentMediaTime(); NSLog(@"CACurrentMediaTime:%f",mediaTime);
返回的就是开机后设备一共运行了(设备休眠不统计在内)多少秒,另一个API也能返回相同的值:
NSTimeIntervalsystemUptime=[[NSProcessInfoprocessInfo]systemUptime]; NSLog(@"systemUptime:%f",systemUptime);
CACurrentMediaTime()也不会受系统时间影响,只受设备重启和休眠行为影响。
sysctl
iOS系统还记录了上次设备重启的时间。可以通过如下API调用获取:
#include<sys/sysctl.h> -(long)bootTime { #defineMIB_SIZE2 intmib[MIB_SIZE]; size_tsize; structtimevalboottime; mib[0]=CTL_KERN; mib[1]=KERN_BOOTTIME; size=sizeof(boottime); if(sysctl(mib,MIB_SIZE,&boottime,&size,NULL,0)!=-1) { returnboottime.tv_sec; } return0; }
返回的值是上次设备重启的Unixtime。
这个API返回的值也会受系统时间影响,用户如果修改时间,值也会随着变化。
有了以上获取时间的各种手段,我们再来看看一些场景之下的具体应用。
场景一,时间测量
我们做性能优化的时候,经常需要对某个方法执行的时间做记录,就必然会用到上面提到的一些获取时间的方法。
在做方法执行时间的benchmark的时候,我们获取时间的方法要满足两个要求,一是精读要高,而是API本身几乎不耗CPU时间。
客户端做性能优化一般是为了主线程的流畅性,而我们知道UI线程如果遇到超过16.7ms的阻塞,就会出现掉帧现象,所以我们关注的时间的精读实际上是在毫秒(ms)级别。我们写客户端代码的时候,基本上都是处于ms这一维度,如果一个方法损耗是0.1ms,我们可以认为这个方法对于流畅性来说是安全的,如果经常看到超过1ms或者几个ms的方法,主线程出现卡顿的几率就会变高。
上面几种获取时间的方式精读上都是足够的,比如一个NSDateAPI调用返回的精读是0.000004S,也就是4微秒,CACurrentMediaTime()返回的精读也在微秒级别,精读上都符合要求。不过有一种看法,认为NSDate属于类的封装,OOP高级语言本身所带来的损耗可能会影响最后的实际结果,在做benchmark的时候不如C函数调用精准,为了验证这一说法,我写了一段简单的测试代码:
inttestCount=10000; doubleavgCost=0; for(inti=0;i<testCount;i++){ NSDate*begin=[NSDatedate]; NSLog(@"ameaninglesslog"); avgCost+=-[begintimeIntervalSinceNow]; } NSLog(@"benchmarkwithNSDate:%f",avgCost/testCount); avgCost=0; for(inti=0;i<testCount;i++){ doublestartTime=CACurrentMediaTime(); NSLog(@"ameaninglesslog"); doubleendTime=CACurrentMediaTime(); avgCost+=(endTime-startTime); } NSLog(@"benchmarkwithCACurrentMediaTime:%f",avgCost/testCount);
输出结果为:
benchmarkwithNSDate:0.000046 benchmarkwithCACurrentMediaTime:0.000037
可以看出CACurrentMediaTime与NSDate代码本身的损耗差异在几微秒,而我们做UI性能优化的维度在毫秒级别,几个微秒的差异完全不会影响我们最后的判断结果。所以使用NSDate做benchmark完全是可行的,以下是我常用的两个宏:
#defineTICKNSDate*startTime=[NSDatedate] #defineTOCKNSLog(@"TimeCost:%f",-[startTimetimeIntervalSinceNow])
场景二:客户端和服务器之间的时间同步
这也是我们经常遇到的场景,比如电商类App到零点的时候开始抢购,比如商品限购倒计时等等,这种场景下需要我们将客户端的时间与服务器保持一致,最重要的是,要防止用户通过断网修改系统时间,来影响客户端的逻辑。
比较普遍的做法是,在一些常用的Server接口里面带上服务器时间,每调用一次接口,客户端就和服务器时间做一次同步并记录下来,但问题是如何防止用户修改呢?
上面提到的NSDate,CFAbsoluteTimeGetCurrent,gettimeofday,sysctl都是跟随系统时间变化的,mach_absolute_time和CACurrentMediaTime虽然是依据CPU时钟数,不受系统时间影响,但在休眠和重启的时候还是会被影响。看上去都不太适合,这里介绍下我个人的做法。
首先还是会依赖于接口和服务器时间做同步,每次同步记录一个serverTime(Unixtime),同时记录当前客户端的时间值lastSyncLocalTime,到之后算本地时间的时候先取curLocalTime,算出偏移量,再加上serverTime就得出时间了:
uint64_trealLocalTime=0; if(serverTime!=0&&lastSyncLocalTime!=0){ realLocalTime=serverTime+(curLocalTime-lastSyncLocalTime); }else{ realLocalTime=[[NSDatedate]timeIntervalSince1970]*1000; }
如果从来没和服务器时间同步过,就只能取本地的系统时间了,这种情况几乎也没什么影响,说明客户端还没开始用过。
关键在于如果获取本地的时间,可以用一个小技巧来获取系统当前运行了多长时间,用系统的运行时间来记录当前客户端的时间:
//getsystemuptimesincelastboot -(NSTimeInterval)uptime { structtimevalboottime; intmib[2]={CTL_KERN,KERN_BOOTTIME}; size_tsize=sizeof(boottime); structtimevalnow; structtimezonetz; gettimeofday(&now,&tz); doubleuptime=-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; } returnuptime; }
gettimeofday和sysctl都会受系统时间影响,但他们二者做一个减法所得的值,就和系统时间无关了。这样就可以避免用户修改时间了。当然用户如果关机,过段时间再开机,会导致我们获取到的时间慢与服务器时间,真实场景中,慢于服务器时间往往影响较小,我们一般担心的是客户端时间快于服务器时间。
多和服务器做时间同步,再把关键的时间校验逻辑放在Server端,就不会出现什么意外的bug了。
总结
关于时间处理的逻辑就总结到这里了,关键还在于我们对于时间本身的理解,对于表达时间的各种方式的理解,理解背后的原理才能选择合适的工具。
感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!