背景
在 iOS 開發中,凡是用到系統時間的,都要考慮一個問題:對時。有些業務是無需對時,或可以以使用者時間為準的,比如動畫用到的時間、一些日程類應用等。但電商相關的業務大都不能直接使用裝置上的時間,而是需要跟伺服器校準後的時間,例如:
- 區間判斷:一些優惠促銷活動需要在 app 端判斷當前是否在活動期間內。如果使用者裝置時間不準,會給使用者錯誤的資訊,導致投訴。
- 倒數計時:各種秒殺、限時促銷、未支付訂單的失效等的倒數計時。如果使用者裝置時間不準,會帶來倒數計時結束後重新整理頁面,狀態沒變化的問題。可以測試一下電商大廠的 app,任意撥表之後倒數計時仍是正確的。
- 同步:如有資料同步的需求,裝置時間不準會造成不能正確判斷資料的新舊關係,可能會讓舊資料覆蓋新資料,造成資料丟失。
- 請求時間戳:對於分頁的資料,為了防止新插入的資料導致翻頁時資料錯亂,一個常見的解決方案是請求列表時加上時間戳的引數,後臺過濾只顯示時間戳之後的資料。如果使用者裝置錶慢了,就會顯示不出最新的資料,導致新發布內容在列表不出現的情況。
可以看出,對時這個需求是非常普遍的。不過實現起來並不難,在這裡分享一下我們的經驗。
解決方案
之所以叫解決方案,是因為這個功能不單是 app 端加幾行程式碼,而是前後端配合完成的。大概思路如下:
- 後端需要做的:每一個網路請求的返回資料都要帶有伺服器當前時間戳
- app 端的網路框架在網路請求的公共回撥處取出時間戳
- 將伺服器時間與本地時間的差值快取到本地
- 需要使用時間時,使用本地時間和快取的時間差,算出相應的伺服器時間
網路請求回撥
伺服器的時間戳可以加在 response body 裡作為公共欄位。在我的專案裡,因為有少量 get 請求,所以放在了 response header 裡。程式碼類似如下:
1 2 3 4 |
+ (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
1 2 3 4 |
- (void)updateServerTime:(long long)timestamp { NSTimeInterval timeInteval = timestamp / 1000.0 - [[NSDate date] timeIntervalSince1970]; [self sharedInstance].timeIntevalDifference = timeInteval; } |
提供校準過的時間
需要使用時間時,根據當前時間和快取過的時間差,計算校準後的時間:
HAMDateTimeUtils.m
1 2 3 4 5 6 7 8 9 10 11 12 |
+ (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:會有一定誤差。原因在於,伺服器返回的時間戳是從伺服器開始返回資料的時間,到客戶端接收時會有一點延遲。不過對於我們的後臺,這個延遲一般 如果對準確性要求更高,可以考慮使用專門的對時介面,不知道國家天文臺有沒有……
另外,這種對時的方案只是用於優化 UI 層面的顯示,不能防止使用者惡意的篡改。要始終記住客戶端的時間戳是不可信的,後端業務凡是使用時間都務必用伺服器的時間。 - Q:快取的時候,為什麼只存在單例裡,不持久化儲存?
A:這個我也考慮過,主要是覺得再次啟動的時候,時間差可能會發生變化,感覺持久化沒有太大的必要。如果覺得有必要的話,也可以在 userDefault 裡存一份,啟動時取出來即可。