iOS 时间校准解决方案

EnezeeChurc 5年前
   <h2>背景</h2>    <p>在 iOS 开发中,凡是用到系统时间的,都要考虑一个问题:对时。有些业务是无需对时,或可以以用户时间为准的,比如动画用到的时间、一些日程类应用等。但电商相关的业务大都不能直接使用设备上的时间,而是需要跟服务器校准后的时间,例如:</p>    <ul>     <li>区间判断:一些优惠促销活动需要在 app 端判断当前是否在活动期间内。如果用户设备时间不准,会给用户错误的信息,导致投诉。</li>     <li>倒计时:各种秒杀、限时促销、未支付订单的失效等的倒计时。如果用户设备时间不准,会带来倒计时结束后刷新页面,状态没变化的问题。可以测试一下电商大厂的 app,任意拨表之后倒计时仍是正确的。</li>     <li>同步:如有数据同步的需求,设备时间不准会造成不能正确判断数据的新旧关系,可能会让旧数据覆盖新数据,造成数据丢失。</li>     <li>请求时间戳:对于分页的数据,为了防止新插入的数据导致翻页时数据错乱,一个常见的解决方案是请求列表时加上时间戳的参数,后台过滤只显示时间戳之后的数据。如果用户设备表慢了,就会显示不出最新的数据,导致新发布内容在列表不出现的情况。</li>    </ul>    <p>可以看出,对时这个需求是非常普遍的。不过实现起来并不难,在这里分享一下我们的经验。</p>    <h2>解决方案</h2>    <p>之所以叫解决方案,是因为这个功能不单是 app 端加几行代码,而是前后端配合完成的。大概思路如下:</p>    <ol>     <li>后端需要做的:每一个网络请求的返回数据都要带有服务器当前时间戳</li>     <li>app 端的网络框架在网络请求的公共回调处取出时间戳</li>     <li>将服务器时间与本地时间的差值缓存到本地</li>     <li>需要使用时间时,使用本地时间和缓存的时间差,算出相应的服务器时间</li>    </ol>    <h3>网络请求回调</h3>    <p>服务器的时间戳可以加在 response body 里作为公共字段。在我的项目里,因为有少量 get 请求,所以放在了 response header 里。代码类似如下:</p>    <pre>  <code class="language-objectivec">+ (void)handleSuccessResponse:(id)responseObjectoperation:(AFHTTPRequestOperation *)operationresponseType:(Class)responseClasssuccess:(void (^)(id))successBlockfailure:(void (^)(NSError *))failureBlock {      long long timestamp = [[operation.response.allHeaderFieldsobjectForKey:@"Response-Timestamp"] longLongValue];      [HAMDateTimeUtilsupdateServerTime:timestamp];  }  </code></pre>    <p>每次网络请求成功时更新时间差的缓存。</p>    <p>一个小的注意点是,处理 timestamp 最好始终用 long long 类型。因为 timestamp 传统上是以毫秒为单位的(虽然在 iOS 这个奇葩系统里 NSTimeInteval 是以秒为单位),在 32 位系统上 long 和 NSInteger 都存不下,会溢出。当然,现在 32 位系统的设备已经不常见了。</p>    <h3>时间差的缓存</h3>    <p>在更新缓存时,把服务器时间与本地当前的时间差保存在单例里。</p>    <p>HAMDateTimeUtils.m</p>    <pre>  <code class="language-objectivec">- (void)updateServerTime:(long long)timestamp {      NSTimeIntervaltimeInteval = timestamp / 1000.0 - [[NSDatedate] timeIntervalSince1970];      [self sharedInstance].timeIntevalDifference = timeInteval;  }  </code></pre>    <h3>提供校准过的时间</h3>    <p>需要使用时间时,根据当前时间和缓存过的时间差,计算校准后的时间:</p>    <p>HAMDateTimeUtils.m</p>    <pre>  <code class="language-objectivec">+ (NSDate*)currentTime {      NSDate* serverDate = [NSDatedateWithTimeIntervalSinceNow:[self sharedInstance].timeIntevalDifference];      return serverDate;  }     // 以毫秒为单位  + (long long)currentTimeStamp {      NSTimeIntervallocalTime = [[NSDatedate] timeIntervalSince1970];      NSTimeIntervaltimeDifference = [WNYDateTimeUtilssharedInstance].timeIntevalDifference;         return (long long)((localTimeStamp + timeDifference) * 1000);  }  </code></pre>    <p>使用时只需调用 [HAMDateTimeUtils currentTime] 或 [HAMDateTimeUtils currentTimeStamp] 即可。</p>    <h2>讨论</h2>    <ul>     <li>Q:这样得出的时间准确吗?<br> A:会有一定误差。原因在于,服务器返回的时间戳是从服务器开始返回数据的时间,到客户端接收时会有一点延迟。不过对于我们的后台,这个延迟一般 如果对准确性要求更高,可以考虑使用专门的对时接口,不知道国家天文台有没有……<br> 另外,这种对时的方案只是用于优化 UI 层面的显示,不能防止用户恶意的篡改。要始终记住客户端的时间戳是不可信的,后端业务凡是使用时间都务必用服务器的时间。</li>     <li>Q:缓存的时候,为什么只存在单例里,不持久化存储?<br> A:这个我也考虑过,主要是觉得再次启动的时候,时间差可能会发生变化,感觉持久化没有太大的必要。如果觉得有必要的话,也可以在 userDefault 里存一份,启动时取出来即可。</li>    </ul>    <p> </p>    <p>来自:http://ios.jobbole.com/91410/</p>    <p> </p>