時(shí)間標(biāo)準(zhǔn)與時(shí)差:
UTC與GMT
UTC是我們現(xiàn)在用得時(shí)間標(biāo)準(zhǔn),GMT是老的時(shí)間計(jì)量標(biāo)準(zhǔn)。UTC是根據(jù)原子鐘來(lái)計(jì)算時(shí)間,GMT是根據(jù)地球的自轉(zhuǎn)和公轉(zhuǎn)來(lái)計(jì)算時(shí)間。(太陽(yáng)所處的位置變化跟地球的自轉(zhuǎn)相關(guān),過(guò)去人們認(rèn)為地球自轉(zhuǎn)的速率是恒定的,但在1960年這一認(rèn)知被推翻了,人們發(fā)現(xiàn)地球自轉(zhuǎn)的速率正變得越來(lái)越慢,而時(shí)間前進(jìn)的速率還是恒定的。)
時(shí)區(qū)
整個(gè)地球分為二十四時(shí)區(qū),每個(gè)時(shí)區(qū)都有自己的本地時(shí)間。在國(guó)際無(wú)線電通信場(chǎng)合,為了統(tǒng)一起見(jiàn),使用一個(gè)統(tǒng)一的時(shí)間,稱為通用協(xié)調(diào)時(shí)(UTC,Universal Time Coordinated)。UTC與GMT一樣,都與英國(guó)倫敦的本地時(shí)相同(倫敦處于0時(shí)區(qū))。北京的20:00和東京的21:00其實(shí)是同一個(gè)絕對(duì)的時(shí)間值。
unix時(shí)間戳
unix時(shí)間戳是從1970年1月1日(UTC/GMT的午夜)開(kāi)始所經(jīng)過(guò)的秒數(shù),不考慮閏秒。
獲取時(shí)間的方式:
1.NSDate
NSDate時(shí)間是UTC標(biāo)準(zhǔn),受手機(jī)系統(tǒng)時(shí)間控制,會(huì)受到用戶改系統(tǒng)時(shí)間影響。
可以使用NSDateFormatter對(duì)日期進(jìn)行字符串格式化。在iOS7及以后,NSDateFormatter是線程安全的,但創(chuàng)建NSDateFormatter對(duì)性能影響大,頻繁使用有必要對(duì)其進(jìn)行緩存。

通過(guò)對(duì)NSDateFormatter增加分類,實(shí)現(xiàn)sharedDateFormatter方法對(duì)NSDateFormatter緩存。


通過(guò)測(cè)試結(jié)果可以發(fā)現(xiàn)NSDateFormatter創(chuàng)建時(shí)間相對(duì)NSOjbect是五倍左右(在早期版本測(cè)試是30倍左右,蘋(píng)果在最新版本應(yīng)該對(duì)NSDateFormatter的創(chuàng)建做了性能優(yōu)化),若對(duì)NSDateFormat進(jìn)行緩存,則可以大大減小NSDateFormatter創(chuàng)建對(duì)象的性能開(kāi)銷。
2.?CFAbsoluteTimeGetCurrent()
CFAbsoluteTimeGetCurrent()與NSDate類似,返回的是相對(duì)2001年1月1日0點(diǎn)的GMT時(shí)間。和NSDate一樣,受手機(jī)系統(tǒng)時(shí)間控制,會(huì)受到用戶改系統(tǒng)時(shí)間影響。
3.?CACurrentMediaTime()
CACurrentMediaTime()返回的是開(kāi)機(jī)后設(shè)備一共運(yùn)行了(設(shè)備休眠不統(tǒng)計(jì)在內(nèi))多少秒,不會(huì)受系統(tǒng)時(shí)間影響,只受設(shè)備重啟和休眠行為影響。
4.?gettimeofday
gettimeofday返回的是?UTC?Unix時(shí)間戳,和[[NSDate date] timeIntervalSince1970]一樣。gettimeofday受當(dāng)前設(shè)備的系統(tǒng)時(shí)間影響。
5.?sysctl
sysctl返回上次設(shè)備重啟的Unix時(shí)間戳。sysctl受當(dāng)前設(shè)備的系統(tǒng)時(shí)間影響。
6. dispatch_benchmark
dispatch_benchmark是GCD里的一個(gè)私有函數(shù),用于測(cè)試代碼運(yùn)行效率,可設(shè)置代碼塊執(zhí)行次數(shù),返回值為unsigned Int64的納秒值。因?yàn)槭撬接泻瘮?shù),使用時(shí)需引入externuint64_tdispatch_benchmark(size_tcount,void(^block)(void));函數(shù)聲明,在app上架前需要移除該函數(shù)。
定時(shí)器類型:
1. NSTimer
Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
按照官方文檔的說(shuō)明,NSTimer是基于RunLoop,RunLoop對(duì)timer持有強(qiáng)引用,所以當(dāng)timer加入到RunLoop以后,你不必對(duì)timer持有強(qiáng)引用(timer不會(huì)被釋放)。
timer對(duì)target是強(qiáng)引用,若target是viewController,并強(qiáng)引用了timer指針,在viewController頁(yè)面關(guān)閉時(shí)容易造成循環(huán)引用。即使target沒(méi)有強(qiáng)引用timer,但timer被Runloop強(qiáng)引用,timer又強(qiáng)引用target,在viewController頁(yè)面關(guān)閉時(shí)也會(huì)造成內(nèi)存泄漏。
timer可通過(guò)fire主動(dòng)觸發(fā)回調(diào),定時(shí)器的下次回調(diào)觸發(fā)時(shí)間不會(huì)受到fire影響,即不會(huì)在fire調(diào)用后重啟計(jì)時(shí)。
timer的Invalidate方法被調(diào)用時(shí),NSRunLoop對(duì)象會(huì)釋放對(duì)timer的持有,同時(shí)timer也會(huì)釋放對(duì)target的持有,避免內(nèi)存泄漏問(wèn)題。
對(duì)于repeat為true的NSTimer必須在定時(shí)器結(jié)束使用時(shí)調(diào)用invalidate方法,避免內(nèi)存泄漏。若repeat為false,會(huì)在定時(shí)器到達(dá)時(shí)間后自動(dòng)解除NSRunLoop和target的持有,如果需要提前解除持有,可以通過(guò)invalidate方法。
NSTimer通過(guò)RunLoop執(zhí)行調(diào)用,因此,若RunLoop任務(wù)過(guò)于繁重,可能會(huì)導(dǎo)致timer不準(zhǔn)時(shí)。每個(gè)Runloop同時(shí)只能處于一種模式,常見(jiàn)?NSDefaultRunLoopMode 默認(rèn)模式 ,UITrackingRunLoopMode 追蹤模式(UIScrollView滑動(dòng)時(shí)),若要timer回調(diào)不受到當(dāng)前模式變化影響,可將timer加入到NSRunLoopCommonModes 復(fù)合模式下(包含NSDefaultRunLoopMode,UITrackingRunLoopMode)。
因?yàn)樽泳€程的RunLoop默認(rèn)是關(guān)閉的,若在子線程啟用NSTimer,必須先開(kāi)啟子線程RunLoop。同時(shí)NSTimer的創(chuàng)建與撤銷必須在同一個(gè)線程操作。
2.?performSelector: withObject: afterDelay:?inModes:
performSelector和NSTimer一樣,都是基于RunLoop,只能實(shí)現(xiàn)非重復(fù)的單次調(diào)用定時(shí)器??梢允褂胏ancelPreviousPerformRequestsWithTarget方法撤銷。
3. CADisplayLink
CADisplayLink是QuartzCore提供的視圖刷新定時(shí)器,基本特性和使用與NSTimer類似,也是基于RunLoop,RunLoop對(duì)CADisplayLink持有強(qiáng)引用。CADisplayLink可通過(guò)frameInterval設(shè)置屏幕刷新多少幀觸發(fā)一次回調(diào),默認(rèn)1。duration是只讀屬性,返回兩次屏幕刷新的時(shí)間間隔。默認(rèn)的觸發(fā)時(shí)機(jī)是每次屏幕需要刷新的時(shí)候,一般是60次/秒,但會(huì)受到設(shè)備卡頓影響。比如iOS設(shè)備執(zhí)行大負(fù)荷運(yùn)算導(dǎo)致刷新降低到50次/秒,CADisplayLink的回調(diào)也是50次/秒觸發(fā)。
使用場(chǎng)景:常用于界面渲染繪制,和界面刷新保持同步進(jìn)行繪制,可以保持過(guò)多繪制導(dǎo)致性能浪費(fèi),也可避免過(guò)少繪制導(dǎo)致的渲染動(dòng)畫(huà)不流暢??捎糜趯?shí)現(xiàn)監(jiān)聽(tīng)屏幕刷新率,功能對(duì)卡頓影響。
4. GCD Timer
GCD Timer 相對(duì)NSTimer有很多優(yōu)點(diǎn)。
1. GCD不會(huì)被調(diào)用者強(qiáng)持有,但在block內(nèi)部需要weakself避免對(duì)self引用。
2.?GCD的定時(shí)器,是依賴內(nèi)核調(diào)用,不依賴于RunLoop,因此更加準(zhǔn)時(shí)。
3. 因?yàn)椴皇腔谀骋痪€程的RunLoop,所以創(chuàng)建與撤銷不需要在同一個(gè)線程操作。
可通過(guò)dispatch_source_cancel主動(dòng)撤銷定時(shí)器。dispatch_resume 啟用定時(shí)器。
因?yàn)镚CD Timer的API比較繁瑣,可以對(duì)其進(jìn)行封裝。

5. GCD?dispatch_after
dispatch_after?只能實(shí)現(xiàn)非重復(fù)的單次調(diào)用定時(shí)器,且啟動(dòng)后不能撤銷。
實(shí)現(xiàn)精準(zhǔn)計(jì)時(shí):
常見(jiàn)如限時(shí)秒殺需求,界面需要通過(guò)定時(shí)器每秒更新秒殺活動(dòng)開(kāi)啟倒計(jì)時(shí),若使用間隔1秒定時(shí)器的實(shí)現(xiàn)方式會(huì)存在以下幾個(gè)問(wèn)題:
1.?NSTimer/GCDTimer計(jì)時(shí)都會(huì)存在延遲,且延遲會(huì)累積,長(zhǎng)時(shí)間計(jì)時(shí)會(huì)導(dǎo)致偏差很大,且app退到后臺(tái)會(huì)導(dǎo)致定時(shí)器暫停。
2. 用戶可通過(guò)修改系統(tǒng)時(shí)間,導(dǎo)致app計(jì)算時(shí)間間隔錯(cuò)誤。
3. 服務(wù)器將服務(wù)器的時(shí)間傳給app客戶端,但因?yàn)榫W(wǎng)絡(luò)延遲,app獲取到的服務(wù)器時(shí)間是網(wǎng)絡(luò)延遲之前的時(shí)間,而不是當(dāng)下的時(shí)間。
4.?間隔1秒定時(shí)器會(huì)帶來(lái)最長(zhǎng)不超過(guò)1秒的定時(shí)器延遲。
解決方案:
1. 這種業(yè)務(wù)場(chǎng)景不要使用1秒定時(shí)器觸發(fā)作為時(shí)間經(jīng)常1秒的依據(jù),可以在收到服務(wù)器時(shí)間serverRecordTime時(shí)記錄啟動(dòng)的時(shí)間startTime,在定時(shí)器觸發(fā)時(shí)計(jì)算現(xiàn)在與啟動(dòng)時(shí)間的間隔interval = now - startTime,現(xiàn)在的服務(wù)器時(shí)間nowServerTime = serverRecordTime +?interval。
2. 用戶可通過(guò)修改系統(tǒng)時(shí)間,導(dǎo)致interval = now - startTime計(jì)算錯(cuò)誤(startTime是在修改時(shí)間前記錄,now在修改時(shí)間后記錄)。
gettimeofday(當(dāng)前時(shí)間)和sysctl(iOS系統(tǒng)上次重啟時(shí)間)都會(huì)受系統(tǒng)時(shí)間影響,若他們二者做一個(gè)減法所得的值,就和系統(tǒng)時(shí)間無(wú)關(guān)。
startTime = 收到服務(wù)器時(shí)間的gettimeofday -?sysctl
now = 現(xiàn)在的gettimeofday -?sysctl
interval = now - startTime
nowServerTime =?serverRecordTime +?interval
3. 上面的計(jì)算方式會(huì)因?yàn)槭盏椒?wù)器時(shí)間網(wǎng)絡(luò)延遲,導(dǎo)致app本地計(jì)時(shí)慢于服務(wù)器。這通常影響并不大,因?yàn)閍pp慢于服務(wù)器并不會(huì)導(dǎo)致用戶在活動(dòng)開(kāi)始前觸發(fā)接口。
如果需要避免網(wǎng)絡(luò)延遲帶來(lái)的計(jì)時(shí)誤差,可以假使用戶沒(méi)有修改系統(tǒng)時(shí)間(絕大部分用戶不會(huì)修改系統(tǒng)時(shí)間),服務(wù)器UTC時(shí)間和手機(jī)系統(tǒng)UTC時(shí)間保持一致。保留上面2方式的同時(shí),在app收到服務(wù)器時(shí)間同時(shí)以本地時(shí)間作為第二個(gè)服務(wù)器時(shí)間serverRecordTime2。后續(xù)使用和2方式一樣的計(jì)算,在用戶沒(méi)有修改系統(tǒng)時(shí)間情況下,網(wǎng)絡(luò)延遲delay =?serverRecordTime2 -?serverRecordTime。
服務(wù)器良好情況下,大部分情況網(wǎng)絡(luò)延遲小于3秒,若serverRecordTime2 -?serverRecordTime < 3秒,認(rèn)為用戶沒(méi)有修改時(shí)間,serverRecordTime2 -?serverRecordTim的值是延遲時(shí)間,則用serverRecordTime2作為最終的服務(wù)器記錄時(shí)間;否則,認(rèn)為用戶可能修改時(shí)間,則用serverRecordTime作為最終的服務(wù)器記錄時(shí)間。
4.?若對(duì)定時(shí)實(shí)時(shí)更新要求高,可以通過(guò)降低定時(shí)器觸發(fā)間隔來(lái)減少定時(shí)器延遲。
NSTimer/CADisplayLink 導(dǎo)致的循環(huán)引用問(wèn)題
如下代碼,會(huì)造成runloop->timer->viewController->timer引起循環(huán)引用,按照蘋(píng)果官方推薦,加入到runloop的timer已經(jīng)被runloop持有,可以使用weak修飾,引用關(guān)系變成runloop->timer->viewController,但雖然這樣可以避免循環(huán)引用,但runloop對(duì)timer的引用關(guān)系在控制器退出后仍然得不到釋放。

一種比較好的解決方式是使用代理對(duì)象NSProxy,蘋(píng)果官方關(guān)于NSProxy的解釋:NSProxy is an abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet. Typically, a message to a proxy is forwarded to the real object or causes the proxy to load (or transform itself into) the real object. Subclasses of NSProxy can be used to implement transparent distributed messaging (for example,?NSDistantObject) or for lazy instantiation of objects that are expensive to create.
NSProxy是個(gè)抽象類(不能直接實(shí)例化對(duì)象,需要在繼承的子類中實(shí)例化),NSProxy可以被用來(lái)轉(zhuǎn)發(fā)消息或者性能耗費(fèi)巨大的對(duì)象的懶加載初始化。
繼承自NSObject的對(duì)象調(diào)用方法,本質(zhì)是發(fā)送消息,可能會(huì)經(jīng)過(guò)方法查找,動(dòng)態(tài)方法解析,消息轉(zhuǎn)發(fā)其他對(duì)象,方法簽名幾個(gè)階段。

而NSProxy并不繼承自NSObject,收到方法調(diào)用后,會(huì)跳過(guò)所有過(guò)程,直接進(jìn)入方法簽名階段。這種特性使其適合專門(mén)的消息轉(zhuǎn)發(fā)場(chǎng)景,避免了前面流程帶來(lái)的性能開(kāi)銷。
使用NSProxy解決timer引用問(wèn)題的本質(zhì)是將引用關(guān)系 runloop->timer->viewController 改變成如下。
runloop->timer->proxy
這樣,viewController并不受到timer強(qiáng)引用,viewController在dealloc中invalidate timer。
NSProxy利用方法簽名,將timer對(duì)其的調(diào)用轉(zhuǎn)發(fā)給viewController,實(shí)現(xiàn)代碼如下:

注:
1. NSObject的init方法中,有對(duì)其初始化的一些操作,因此實(shí)例化對(duì)象時(shí)必須調(diào)用init。而NSProxy并不繼承自NSObject,子類實(shí)例化時(shí)沒(méi)有也不需要調(diào)用init方法。
2. weak引用的target隨時(shí)可能被釋放變成nil,導(dǎo)致methodSignatureForSelector方法返回nil,進(jìn)而導(dǎo)致crash。因此在methodSignatureForSelector方法中加入判斷,當(dāng)target指向nil,返回任意NSMethodSignature,并在forwardInvocation中不處理。
3. NSProxy中將forwardingTargetForSelector方法注釋了不對(duì)外公開(kāi),但是通過(guò)測(cè)試發(fā)現(xiàn),實(shí)現(xiàn)forwardingTargetForSelector方法仍然有效,成功進(jìn)行消息轉(zhuǎn)發(fā)。但蘋(píng)果Foundation中既然已經(jīng)對(duì)外不公開(kāi),因此不推薦使用,建議仍然使用methodSignatureForSelector。
