背景

在 iOS 开发中,凡是用到系统时间的,都要考虑一个问题:对时。有些业务是无需对时,或可以以用户时间为准的,比如动画用到的时间、一些日程类应用等。但电商相关的业务大都不能直接使用设备上的时间,而是需要跟服务器校准后的时间,例如:

  • 区间判断:一些优惠促销活动需要在 app 端判断当前是否在活动期间内。如果用户设备时间不准,会给用户错误的信息,导致投诉。
  • 倒计时:各种秒杀、限时促销、未支付订单的失效等的倒计时。如果用户设备时间不准,会带来倒计时结束后刷新页面,状态没变化的问题。可以测试一下电商大厂的 app,任意拨表之后倒计时仍是正确的。
  • 同步:如有数据同步的需求,设备时间不准会造成不能正确判断数据的新旧关系,可能会让旧数据覆盖新数据,造成数据丢失。
  • 请求时间戳:对于分页的数据,为了防止新插入的数据导致翻页时数据错乱,一个常见的解决方案是请求列表时加上时间戳的参数,后台过滤只显示时间戳之后的数据。如果用户设备表慢了,就会显示不出最新的数据,导致新发布内容在列表不出现的情况。

可以看出,对时这个需求是非常普遍的。不过实现起来并不难,在这里分享一下我们的经验。

解决方案

之所以叫解决方案,是因为这个功能不单是 app 端加几行代码,而是前后端配合完成的。大概思路如下:

  1. 后端需要做的:每一个网络请求的返回数据都要带有服务器当前时间戳
  2. app 端的网络框架在网络请求的公共回调处取出时间戳
  3. 将服务器时间与本地时间的差值缓存到本地
  4. 需要使用时间时,使用本地时间和缓存的时间差,算出相应的服务器时间

网络请求回调

服务器的时间戳可以加在 response body 里作为公共字段。在我的项目里,因为有少量 get 请求,所以放在了 response header 里。代码类似如下:

+ (void)handleSuccessResponse:(id)responseObject operation:(AFHTTPRequestOperation *)operation responseType:(Class)responseClass success:(void (^)(id))successBlock failure:(void (^)(NSError *))failureBlock {
long long timestamp = [[operation.response.allHeaderFields objectForKey:@"Response-Timestamp"] longLongValue];
[HAMDateTimeUtils updateServerTime:timestamp];
}

每次网络请求成功时更新时间差的缓存。

一个小的注意点是,处理 timestamp 最好始终用 long long 类型。因为 timestamp 传统上是以毫秒为单位的(虽然在 iOS 这个奇葩系统里 NSTimeInteval 是以秒为单位),在 32 位系统上 long 和 NSInteger 都存不下,会溢出。当然,现在 32 位系统的设备已经不常见了。

时间差的缓存

在更新缓存时,把服务器时间与本地当前的时间差保存在单例里。

HAMDateTimeUtils.m
- (void)updateServerTime:(long long)timestamp {
NSTimeInterval timeInteval = timestamp / 1000.0 - [[NSDate date] timeIntervalSince1970];
[self sharedInstance].timeIntevalDifference = timeInteval;
}

提供校准过的时间

需要使用时间时,根据当前时间和缓存过的时间差,计算校准后的时间:

HAMDateTimeUtils.m
+ (NSDate*)currentTime {
NSDate* serverDate = [NSDate dateWithTimeIntervalSinceNow:[self sharedInstance].timeIntevalDifference];
return serverDate;
} // 以毫秒为单位
+ (long long)currentTimeStamp {
NSTimeInterval localTime = [[NSDate date] timeIntervalSince1970];
NSTimeInterval timeDifference = [WNYDateTimeUtils sharedInstance].timeIntevalDifference; return (long long)((localTimeStamp + timeDifference) * 1000);
}

使用时只需调用 [HAMDateTimeUtils currentTime] 或 [HAMDateTimeUtils currentTimeStamp] 即可。

讨论

  • Q:这样得出的时间准确吗?
    A:会有一定误差。原因在于,服务器返回的时间戳是从服务器开始返回数据的时间,到客户端接收时会有一点延迟。不过对于我们的后台,这个延迟一般 <100 ms,对于我们的业务来说没什么影响。
    如果对准确性要求更高,可以考虑使用专门的对时接口,不知道国家天文台有没有……
    另外,这种对时的方案只是用于优化 UI 层面的显示,不能防止用户恶意的篡改。要始终记住客户端的时间戳是不可信的,后端业务凡是使用时间都务必用服务器的时间。

  • Q:缓存的时候,为什么只存在单例里,不持久化存储?
    A:这个我也考虑过,主要是觉得再次启动的时候,时间差可能会发生变化,感觉持久化没有太大的必要。如果觉得有必要的话,也可以在 userDefault 里存一份,启动时取出来即可。

iOS 时间校准解决方案的更多相关文章

  1. iOS时间问题

    在iOS开发中,经常会遇到各种各样的时间问题,8小时时差,时间戳,求时间间隔,农历等等.解决办法网上比比皆是,但大多零零散散,很多资料并没有说明其中问题.这里集中总结一下,以便于以后查阅和供大家参考. ...

  2. [控件]unigui移动端下Unidatepicker时间显示解决方案

    [控件]unigui移动端下Unidatepicker时间显示解决方案 http://tz10000.com/kong-jian-unigui-yi-dong-duan-xia-unidatepick ...

  3. Quartz定时任务和IIS程序池闲置超时时间冲突解决方案

    一.问题描述 Bs项目中用Quartz功能执行一个定时任务(每隔5分钟执行一个Job),正常情况,Quartz定时任务会5分钟执行一次,但IIS程序池闲置 超时默认为20分钟,造成的结果是:定时任务只 ...

  4. 支持WEB、Android、IOS的地图解决方案

    转自原文 支持WEB.Android.IOS的地图解决方案 工具链 GIS工具集 OpenGeo Suite 包含PostGIS, GeoServer, GeoWebCache, OpenLayers ...

  5. ios archives 出现的是other items而不是iOS Apps的解决方案

    ios archives 出现的是other items而不是iOS Apps的解决方案 项目打包时出现的是不是出现在iOS Apps栏目下面,而是Other Items而且右边对应的Upload t ...

  6. iOS时间那点事儿–NSTimeZone

    NSTimeZone **时区是一个地理名字,是为了克服各个地区或国家之间在使用时间上的混乱. 基本概念: GMT 0:00 格林威治标准时间; UTC +00:00 校准的全球时间; CCD +08 ...

  7. iOS 时间处理(转)

    NSDate NSDate对象用来表示一个具体的时间点. NSDate是一个类簇,我们所使用的NSDate对象,都是NSDate的私有子类的实体. NSDate存储的是GMT时间,使用的时候会根据 当 ...

  8. 【ntp时间校准配置】

    Ntp(网络时间协议)是一种可以通过TCP/IP网络传播,其架构模式可分为C/S(客户端/服务器),PTP(对等),broatcast(广播), mutilbrocast(组播),无论在任何系统或设备 ...

  9. iOS 循环引用解决方案

    一.BLOCK 循环引用 一般表现为,某个类将block作为自己的属性变量,然后该类在block的方法体里面又使用了该类本身.构成循环引用. // 定义 block 的时候,会对外部变量做一次 cop ...

随机推荐

  1. React Native组件之BackAndroid !安卓手机的物理返回键的使用

    ok!在安卓手机上,当我们用物理返回键的时候,会以一次性的将程序退出来,这样是很不好的体验,所以就需要使用RN的物理返回键组件:BackAndroid,其原理也就是 分析路由,然后pop()这样! o ...

  2. virtualbox中的虚拟机和windows共享文件夹

    http://www.jianshu.com/p/4e3c8b06cb06 为什么要共享文件夹? 在工作的过程当中会使用到不同的软件开发环境,php的,python的,nodejs的为了隔离这些应用环 ...

  3. JavaScript 局部刷新

    JavaScript局部刷新具体代码展示如下 1.  #tabList代表需要刷新的元素的对象 2.  第二个#tabList 如果后面有第三个元素,那么后面需要加>*符号,如果不加,容易造成C ...

  4. RxJava + Retrofit

    一.添加依赖 compile 'io.reactivex:rxandroid:1.2.0' compile 'io.reactivex:rxjava:1.1.5' compile 'com.googl ...

  5. node+ts的心得与坑

    首先先明确,用node+ts的目的,为什么不ng+ts.这一点后面还会反复提醒自己 node毕竟不是ng. 用node的理由: 处理js,在后端操纵dom,读写类html格式的东西,比直接用py的后端 ...

  6. Qt5获取本机网络信息

    获取本机网络信息 在pro文件中加入如下代码 QT += network widget.h中的代码如下 #ifndef WIDGET_H #define WIDGET_H #include <Q ...

  7. AD中设置PCB线间距

    Design->Rules->Electrical->Clearance->Clearance

  8. Unity 通过代码简单实现文理的灰化显示

    1.可以用于纹理的处理,也可用于模型显示的处理(比如某件准备或者服饰未获取的时候,灰化显示) 线上对比图:                     using System.Collections; ...

  9. 让 Ubuntu 16 开机自动启动 Vino Server

    Vino Server 有一个问题, 如果用户没有login , 它是不会启动的. 但是,我把帐号设置从自动启动之后,Vino Server还是没有启动. 看来自动启动跟输密码启动还是有差别的. 具体 ...

  10. learn python the hard way 习题1~5总结

    习题1 print 语句print('Yay! Printing.')print('I "said" do not touch this') 习题2:注释和 # 号 #(octot ...