前言
本文將用到的科普知識如下:
- GMT:(Greenwich Mean Time)格林尼治標準時間。這是以英國格林尼治天文臺觀測結(jié)果得出的時間,這是英國格林尼治當?shù)貢r間,這個地方的當?shù)貢r間過去被當成世界標準的時間。
- UT:(Universal Time)世界時。根據(jù)原子鐘計算出來的時間
- UTC:(Coordinated Universal Time)太陽所處的位置變化跟地球的自轉(zhuǎn)相關(guān),過去人們認為地球自轉(zhuǎn)的速率是恒定的,但在1960年這一認知被推翻了,人們發(fā)現(xiàn)地球自轉(zhuǎn)的速率正變得越來越慢,而時間前進的速率還是恒定的,所以UTC不再被認為可以用來精準的描述時間了。我們需要繼續(xù)尋找一個勻速前進的值。抬頭看天是我們從宏觀方向去尋找答案,科技的發(fā)展讓我們在微觀方面取得了更深的認識,于是有聰明人根據(jù)微觀粒子原子的物理屬性,建立了原子鐘,以這種原子鐘來衡量時間的變化,原子鐘50億年才會誤差1秒,這種精讀已經(jīng)遠勝于GMT了。這個原子鐘所反映的時間,也就是我們現(xiàn)在所使用的UTC(Coordinated Universal Time )標準時間。
場景描述
最近開發(fā)過程中QA同學(xué)提了一個bug, 當手機日期時間修改后 發(fā)現(xiàn)頁面時間顯示異常, 這種問題非常經(jīng)典, 也就是iOS關(guān)于時間的處理.
我們對時間的認識
時間是線性的,即任意一個時刻,這個地球上只有一個絕對時間值存在,只不過因為時區(qū)或者文化的差異,處于同一時空的我們對同一時間的表述或者理解不同。比如,北京的20:00和東京的21:00其實是同一個絕對的時間值。
可以理解為 以一個標準點作為標準點.通過時區(qū)微調(diào) 來實現(xiàn)全球各個國家的日期顯示.
iOS幾種獲取時間的方式
1.NSDate
代碼實現(xiàn)
(void)timeIntervalSinceReferenceDate {
NSDate *date = [NSDate date];
NSLog(@"date = %lf", date.timeIntervalSinceReferenceDate);
}
NSDate對象封裝單個時間點,與任何特定的日歷系統(tǒng)或時區(qū)無關(guān).日期對象是不可變的,表示相對于絕對參考日期(2001年1月1日00:00:00 UTC)的不變時間間隔,它是以UTC為標準的。
NSDate輸出結(jié)果:
2020-12-06 12:28:55.795929+0800 ZGTimeDemo[12177:134289] date = 628921735.795845
下面計算一下:628921735.795845/365/86400 = 19.942977,今年是2020年,距離2001年正好是19年.
如果我們直接打印NSDate
NSDate *date = [NSDate date];
NSLog(@"%@",date);
則會輸出
2020-12-06 06:51:04 +0000
可見NSDate輸出的是絕對的UTC時間,而北京時間的時區(qū)為UTC+8,上面的輸出+8個小時,剛好就是我當前的時間了。所以正常UTC + 時區(qū)才是真正的時間日期. 至于時區(qū)加減請參考下圖.

注意: NSDate是受手機系統(tǒng)時間控制的,當你修改了手機上的時間顯示,NSDate獲取當前時間的輸出也會隨之改變。在我們做App的時候,明白這一點,就知道NSDate并不可靠,因為用戶可能會修改它的值.
2.函數(shù)CFAbsoluteTimeGetCurrent()
官方文檔: 絕對時間是相對于絕對參考日期(格林尼治標準時間2001年1月1日00時00分)以秒計算的。正值表示引用日期之后的日期,負值表示引用日期之前的日期。例如,絕對時間-32940326相當于1999年12月16日17:54:34。重復(fù)調(diào)用這個函數(shù)不能保證單調(diào)遞增的結(jié)果。系統(tǒng)時間可能由于與外部時間引用同步或由于顯式的用戶更改時鐘而減少。
CFAbsoluteTimeGetCurrent()的概念和NSDate非常相似,只不過參考點是:以GMT為標準的,2001年一月一日00:00:00這一刻的時間絕對值。
注意:CFAbsoluteTimeGetCurrent()也會跟著當前設(shè)備的系統(tǒng)時間一起變化,也可能會被用戶修改.
3.gettimeofday()
int gettimeofday(struct timeval * __restrict, void * __restrict);
這個函數(shù)獲取的是UNIX time.
struct timeval now;
struct timezone tz;
gettimeofday(&now, &tz);
NSLog(@"gettimeofday: %ld", now.tv_sec);
gettimeofday: 1607238723
UNIX time又是什么呢?
Unix time是以UTC 1970年1月1號 00:00:00為基準時間,當前時間距離基準點偏移的秒數(shù)。上述API返回的值是1607238723,表示當前時間距離UTC 1970年1月1號 00:00:00一共過了1607238723秒。
Unix time也是平時我們使用較多的一個時間標準,在Mac的終端可以通過以下命令轉(zhuǎn)換成可閱讀的時間:
date -r 1607238723
輸出
2020年12月 6日 星期日 15時12分03秒 CST
注意:gettimeofday(),NSDate,CFAbsoluteTimeGetCurrent這三個都是受當前設(shè)備的系統(tǒng)時間影響.只不過是參考的時間基準點不一樣而已。我們和服務(wù)器通訊的時候一般使用NIX time.
5.mach_absolute_time()
在我們的iPhone上剛好有一個這樣的值存在,它就是CPU的時鐘周期數(shù)(ticks),這個tick的數(shù)值可以用來描述時間,而mach_absolute_time()返回的就是CPU已經(jīng)運行的tick的數(shù)量。將這個tick數(shù)經(jīng)過一定的轉(zhuǎn)換就可以變成秒數(shù),或者納秒數(shù).這樣就和時間直接關(guān)聯(lián)了.不過這個tick數(shù),在每次手機重啟之后,會重新開始計數(shù),而且iPhone鎖屏進入休眠之后tick也會暫停計數(shù).
注意: mach_absolute_time()不會受系統(tǒng)時間影響,只受設(shè)備重啟和休眠行為影響
6.CACurrentMediaTime()
CACurrentMediaTime()就是將上面mach_absolute_time()的CPUtick數(shù)轉(zhuǎn)化成秒數(shù)的結(jié)果。以下代碼:
double mediaTime = CACurrentMediaTime();
NSLog(@"CACurrentMediaTime: %f", mediaTime);
'>2020-12-06 15:34:59.808799+0800 ZGTimeDemo[19731:281283] CACurrentMediaTime: 17789.582767
返回的就是開機后設(shè)備一共運行了(設(shè)備休眠不統(tǒng)計在內(nèi))多少秒.
這個API等同于下面代碼:
NSTimeInterval systemUptime = [[NSProcessInfo processInfo] systemUptime];
注意:CACurrentMediaTime()也不會受系統(tǒng)時間影響,只受設(shè)備重啟和休眠行為影響.
7.sysctl()
iOS系統(tǒng)還記錄了上次設(shè)備重啟的時間??梢酝ㄟ^如下API調(diào)用獲取:
#include <sys/sysctl.h> - (long)bootTime
{
#define MIB_SIZE 2
int mib[MIB_SIZE];
size_t size;
struct timeval boottime;
mib[0] = CTL_KERN;
mib[1] = KERN_BOOTTIME;
size = sizeof(boottime);
if (sysctl(mib, MIB_SIZE, &boottime, &size, NULL, 0) != -1)
{
return boottime.tv_sec;
}
return 0;
}
返回的值是上次設(shè)備重啟的Unix time。
注意:這個API返回的值也會受系統(tǒng)時間影響,用戶如果修改時間,值也會隨著變化.
客戶端和服務(wù)器之間的時間同步
一般我們發(fā)起請求的時候都是在公參中帶上本地時間,如果有一些比較敏感的接口會遇到用戶更改系統(tǒng)時間的異常case導(dǎo)致異常.為了防止用戶通過斷網(wǎng)修改系統(tǒng)時間,來影響客戶端的邏輯我們通常都這樣做.
- 獲取服務(wù)器某一時刻
A的時間; - 記錄獲取到時刻
A時的本地時間B; - 需要用到時間時,獲取當前本地時間
C,當C-B作為時間間隔D,則A+D則是當前服務(wù)器的時間.
這里要準確做到客戶端時間和服務(wù)器時間一致,很關(guān)鍵的問題就是B和C不能受系統(tǒng)時間的影響,要解決這個問題,要依靠iOS的接口–系統(tǒng)運行時間
首先: 我們要依靠服務(wù)端給一個準確的時間戳.每次同步記錄一個得到服務(wù)端時間戳B.我們就是要用運行的時間差來解決時間校時問題.
獲取系統(tǒng)當前運行了多長時間方法:
//get system uptime since last boot
- (NSTimeInterval)uptime
{
struct timeval boottime;
int mib[2] = {CTL_KERN, KERN_BOOTTIME};
size_t size = sizeof(boottime);
struct timeval now;
struct timezone tz;
gettimeofday(&now, &tz);
double uptime = -1;
if (sysctl(mib, 2, &boottime, &size, NULL, 0) != -1 && boottime.tv_sec != 0)
{
uptime = now.tv_sec - boottime.tv_sec;
uptime += (double)(now.tv_usec - boottime.tv_usec) / 1000000.0;
}
return uptime;
}
注意:這個函數(shù)返回的是秒.和server返回的unix time可能要乘以1000.(1s = 1000ms)
gettimeofday()和sysctl()都會受系統(tǒng)時間影響,但他們二者做一個減法所得的值,就和系統(tǒng)時間無關(guān)了.這樣就可以避免用戶修改時間了。當然用戶如果關(guān)機,過段時間再開機,會導(dǎo)致我們獲取到的時間慢與服務(wù)器時間,真實場景中,慢于服務(wù)器時間往往影響較小,我們一般擔(dān)心的是客戶端時間快于服務(wù)器時間.
總結(jié)
本篇問題的解決難點的關(guān)鍵在于如果獲取本地的時間,我們這里取的是系統(tǒng)運行時間進行的差值計算法.我沒有嘗試過 休眠 退后臺等邏輯消耗的時長.但是我認為如果要做好工具類,要嘗試計算后臺消耗的時間計時時長,可以也可以通過系統(tǒng)運行時間的差值運算得到準確的時間.
本篇重點: ABCD同步時間算法 主要依賴于服務(wù)端給的時間作為基準點. 另一個難點怎么獲取系統(tǒng)運行時間做差值計算 來解決系統(tǒng)時間被用戶修改后時間不準的問題.
推薦文集
收錄:原文地址