iOS时间与定时器

时间标准与时差:

UTC与GMT

UTC是我们现在用得时间标准,GMT是老的时间计量标准。UTC是根据原子钟来计算时间,GMT是根据地球的自转和公转来计算时间。(太阳所处的位置变化跟地球的自转相关,过去人们认为地球自转的速率是恒定的,但在1960年这一认知被推翻了,人们发现地球自转的速率正变得越来越慢,而时间前进的速率还是恒定的。)

时区

整个地球分为二十四时区,每个时区都有自己的本地时间。在国际无线电通信场合,为了统一起见,使用一个统一的时间,称为通用协调时(UTC,Universal Time Coordinated)。UTC与GMT一样,都与英国伦敦的本地时相同(伦敦处于0时区)。北京的20:00和东京的21:00其实是同一个绝对的时间值。

unix时间戳

unix时间戳是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒。

获取时间的方式:

1.NSDate

NSDate时间是UTC标准,受手机系统时间控制,会受到用户改系统时间影响。

可以使用NSDateFormatter对日期进行字符串格式化。在iOS7及以后,NSDateFormatter是线程安全的,但创建NSDateFormatter对性能影响大,频繁使用有必要对其进行缓存。

缓存DateFormatter

通过对NSDateFormatter增加分类,实现sharedDateFormatter方法对NSDateFormatter缓存。


DateFormatter性能测试
测试结果

通过测试结果可以发现NSDateFormatter创建时间相对NSOjbect是五倍左右(在早期版本测试是30倍左右,苹果在最新版本应该对NSDateFormatter的创建做了性能优化),若对NSDateFormat进行缓存,则可以大大减小NSDateFormatter创建对象的性能开销。

2. CFAbsoluteTimeGetCurrent()

CFAbsoluteTimeGetCurrent()与NSDate类似,返回的是相对2001年1月1日0点的GMT时间。和NSDate一样,受手机系统时间控制,会受到用户改系统时间影响。

3. CACurrentMediaTime()

CACurrentMediaTime()返回的是开机后设备一共运行了(设备休眠不统计在内)多少秒,不会受系统时间影响,只受设备重启和休眠行为影响。

4. gettimeofday

gettimeofday返回的是 UTC Unix时间戳,和[[NSDate date] timeIntervalSince1970]一样。gettimeofday受当前设备的系统时间影响。

5. sysctl

sysctl返回上次设备重启的Unix时间戳。sysctl受当前设备的系统时间影响。

6. dispatch_benchmark

dispatch_benchmark是GCD里的一个私有函数,用于测试代码运行效率,可设置代码块执行次数,返回值为unsigned Int64的纳秒值。因为是私有函数,使用时需引入externuint64_tdispatch_benchmark(size_tcount,void(^block)(void));函数声明,在app上架前需要移除该函数。

定时器类型:

1. NSTimer

Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

按照官方文档的说明,NSTimer是基于RunLoop,RunLoop对timer持有强引用,所以当timer加入到RunLoop以后,你不必对timer持有强引用(timer不会被释放)。

timer对target是强引用,若target是viewController,并强引用了timer指针,在viewController页面关闭时容易造成循环引用。即使target没有强引用timer,但timer被Runloop强引用,timer又强引用target,在viewController页面关闭时也会造成内存泄漏。

timer可通过fire主动触发回调,定时器的下次回调触发时间不会受到fire影响,即不会在fire调用后重启计时。

timer的Invalidate方法被调用时,NSRunLoop对象会释放对timer的持有,同时timer也会释放对target的持有,避免内存泄漏问题。

对于repeat为true的NSTimer必须在定时器结束使用时调用invalidate方法,避免内存泄漏。若repeat为false,会在定时器到达时间后自动解除NSRunLoop和target的持有,如果需要提前解除持有,可以通过invalidate方法。

NSTimer通过RunLoop执行调用,因此,若RunLoop任务过于繁重,可能会导致timer不准时。每个Runloop同时只能处于一种模式,常见 NSDefaultRunLoopMode 默认模式 ,UITrackingRunLoopMode 追踪模式(UIScrollView滑动时),若要timer回调不受到当前模式变化影响,可将timer加入到NSRunLoopCommonModes 复合模式下(包含NSDefaultRunLoopMode,UITrackingRunLoopMode)。

因为子线程的RunLoop默认是关闭的,若在子线程启用NSTimer,必须先开启子线程RunLoop。同时NSTimer的创建与撤销必须在同一个线程操作。

2. performSelector: withObject: afterDelay: inModes:

performSelector和NSTimer一样,都是基于RunLoop,只能实现非重复的单次调用定时器。可以使用cancelPreviousPerformRequestsWithTarget方法撤销。

3. CADisplayLink

CADisplayLink是QuartzCore提供的视图刷新定时器,基本特性和使用与NSTimer类似,也是基于RunLoop,RunLoop对CADisplayLink持有强引用。CADisplayLink可通过frameInterval设置屏幕刷新多少帧触发一次回调,默认1。duration是只读属性,返回两次屏幕刷新的时间间隔。默认的触发时机是每次屏幕需要刷新的时候,一般是60次/秒,但会受到设备卡顿影响。比如iOS设备执行大负荷运算导致刷新降低到50次/秒,CADisplayLink的回调也是50次/秒触发。

使用场景:常用于界面渲染绘制,和界面刷新保持同步进行绘制,可以保持过多绘制导致性能浪费,也可避免过少绘制导致的渲染动画不流畅。可用于实现监听屏幕刷新率,功能对卡顿影响。

4. GCD Timer

GCD Timer 相对NSTimer有很多优点。

1. GCD不会被调用者强持有,但在block内部需要weakself避免对self引用。

2. GCD的定时器,是依赖内核调用,不依赖于RunLoop,因此更加准时。

3. 因为不是基于某一线程的RunLoop,所以创建与撤销不需要在同一个线程操作。

可通过dispatch_source_cancel主动撤销定时器。dispatch_resume 启用定时器。

因为GCD Timer的API比较繁琐,可以对其进行封装。


封装GCD Timer

5. GCD dispatch_after

dispatch_after 只能实现非重复的单次调用定时器,且启动后不能撤销。

实现精准计时:

常见如限时秒杀需求,界面需要通过定时器每秒更新秒杀活动开启倒计时,若使用间隔1秒定时器的实现方式会存在以下几个问题:

1. NSTimer/GCDTimer计时都会存在延迟,且延迟会累积,长时间计时会导致偏差很大,且app退到后台会导致定时器暂停。

2. 用户可通过修改系统时间,导致app计算时间间隔错误。

3. 服务器将服务器的时间传给app客户端,但因为网络延迟,app获取到的服务器时间是网络延迟之前的时间,而不是当下的时间。

4. 间隔1秒定时器会带来最长不超过1秒的定时器延迟。

解决方案:

1. 这种业务场景不要使用1秒定时器触发作为时间经常1秒的依据,可以在收到服务器时间serverRecordTime时记录启动的时间startTime,在定时器触发时计算现在与启动时间的间隔interval = now - startTime,现在的服务器时间nowServerTime = serverRecordTime + interval。

2. 用户可通过修改系统时间,导致interval = now - startTime计算错误(startTime是在修改时间前记录,now在修改时间后记录)。

gettimeofday(当前时间)和sysctl(iOS系统上次重启时间)都会受系统时间影响,若他们二者做一个减法所得的值,就和系统时间无关。

startTime = 收到服务器时间的gettimeofday - sysctl

now = 现在的gettimeofday - sysctl

interval = now - startTime

nowServerTime = serverRecordTime + interval

3. 上面的计算方式会因为收到服务器时间网络延迟,导致app本地计时慢于服务器。这通常影响并不大,因为app慢于服务器并不会导致用户在活动开始前触发接口。

如果需要避免网络延迟带来的计时误差,可以假使用户没有修改系统时间(绝大部分用户不会修改系统时间),服务器UTC时间和手机系统UTC时间保持一致。保留上面2方式的同时,在app收到服务器时间同时以本地时间作为第二个服务器时间serverRecordTime2。后续使用和2方式一样的计算,在用户没有修改系统时间情况下,网络延迟delay = serverRecordTime2 - serverRecordTime。

服务器良好情况下,大部分情况网络延迟小于3秒,若serverRecordTime2 - serverRecordTime < 3秒,认为用户没有修改时间,serverRecordTime2 - serverRecordTim的值是延迟时间,则用serverRecordTime2作为最终的服务器记录时间;否则,认为用户可能修改时间,则用serverRecordTime作为最终的服务器记录时间。

4. 若对定时实时更新要求高,可以通过降低定时器触发间隔来减少定时器延迟。

NSTimer/CADisplayLink 导致的循环引用问题

如下代码,会造成runloop->timer->viewController->timer引起循环引用,按照苹果官方推荐,加入到runloop的timer已经被runloop持有,可以使用weak修饰,引用关系变成runloop->timer->viewController,但虽然这样可以避免循环引用,但runloop对timer的引用关系在控制器退出后仍然得不到释放。

循环引用问题

一种比较好的解决方式是使用代理对象NSProxy,苹果官方关于NSProxy的解释:NSProxy is an abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet. Typically, a message to a proxy is forwarded to the real object or causes the proxy to load (or transform itself into) the real object. Subclasses of NSProxy can be used to implement transparent distributed messaging (for example, NSDistantObject) or for lazy instantiation of objects that are expensive to create.

NSProxy是个抽象类(不能直接实例化对象,需要在继承的子类中实例化),NSProxy可以被用来转发消息或者性能耗费巨大的对象的懒加载初始化。

继承自NSObject的对象调用方法,本质是发送消息,可能会经过方法查找,动态方法解析,消息转发其他对象,方法签名几个阶段。


消息转发流程

而NSProxy并不继承自NSObject,收到方法调用后,会跳过所有过程,直接进入方法签名阶段。这种特性使其适合专门的消息转发场景,避免了前面流程带来的性能开销。

使用NSProxy解决timer引用问题的本质是将引用关系 runloop->timer->viewController 改变成如下。

runloop->timer->proxy

这样,viewController并不受到timer强引用,viewController在dealloc中invalidate timer。

NSProxy利用方法签名,将timer对其的调用转发给viewController,实现代码如下:

Proxy实现

注:

1. NSObject的init方法中,有对其初始化的一些操作,因此实例化对象时必须调用init。而NSProxy并不继承自NSObject,子类实例化时没有也不需要调用init方法。

2. weak引用的target随时可能被释放变成nil,导致methodSignatureForSelector方法返回nil,进而导致crash。因此在methodSignatureForSelector方法中加入判断,当target指向nil,返回任意NSMethodSignature,并在forwardInvocation中不处理。

3. NSProxy中将forwardingTargetForSelector方法注释了不对外公开,但是通过测试发现,实现forwardingTargetForSelector方法仍然有效,成功进行消息转发。但苹果Foundation中既然已经对外不公开,因此不推荐使用,建议仍然使用methodSignatureForSelector。

你可能感兴趣的:(iOS时间与定时器)