iOS 時間校準解決方案

背景

在 iOS 開發中,凡是用到系統時間的,都要考慮一個問題:對時。有些業務是無需對時,或能夠以用戶時間爲準的,好比動畫用到的時間、一些日程類應用等。但電商相關的業務大都不能直接使用設備上的時間,而是須要跟服務器校準後的時間,例如:javascript

  • 區間判斷:一些優惠促銷活動須要在 app 端判斷當前是否在活動期間內。若是用戶設備時間不許,會給用戶錯誤的信息,致使投訴。
  • 倒計時:各類秒殺、限時促銷、未支付訂單的失效等的倒計時。若是用戶設備時間不許,會帶來倒計時結束後刷新頁面,狀態沒變化的問題。能夠測試一下電商大廠的 app,任意撥表以後倒計時還是正確的。
  • 同步:若有數據同步的需求,設備時間不許會形成不能正確判斷數據的新舊關係,可能會讓舊數據覆蓋新數據,形成數據丟失。
  • 請求時間戳:對於分頁的數據,爲了防止新插入的數據致使翻頁時數據錯亂,一個常見的解決方案是請求列表時加上時間戳的參數,後臺過濾只顯示時間戳以後的數據。若是用戶設備錶慢了,就會顯示不出最新的數據,致使新發布內容在列表不出現的狀況。

能夠看出,對時這個需求是很是廣泛的。不過實現起來並不難,在這裏分享一下咱們的經驗。java

解決方案

之因此叫解決方案,是由於這個功能不單是 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 位系統的設備已經不常見了。網絡

時間差的緩存

在更新緩存時,把服務器時間與本地當前的時間差保存在單例裏。app

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 裏存一份,啓動時取出來便可。

相關文章
相關標籤/搜索