一. 問(wèn)題背景
最近項(xiàng)目中有個(gè)定時(shí)器計(jì)時(shí)實(shí)時(shí)更新等車的時(shí)長(zhǎng),因?yàn)轫?xiàng)目里面進(jìn)入后臺(tái)是有執(zhí)行一些任務(wù)的操作,因此如果進(jìn)入后臺(tái)時(shí)間不長(zhǎng),是定時(shí)器是不會(huì)暫停的,但如果進(jìn)入后臺(tái)時(shí)間,超過(guò)20s以上,定時(shí)器就暫停,回到前臺(tái)重新開(kāi)始倒計(jì)時(shí),這時(shí)候等車的時(shí)長(zhǎng)會(huì)出現(xiàn)不準(zhǔn)的情況。
二. 問(wèn)題原因
經(jīng)驗(yàn)證NSTimer,CADisplayLink,dispatch_source_t,三個(gè)定時(shí)器,在進(jìn)入到后臺(tái)的時(shí)候,都會(huì)暫停,等到返回前臺(tái)的時(shí)候,才會(huì)繼續(xù)回調(diào)。

看了一些博客說(shuō)加上后臺(tái)任務(wù)執(zhí)行這句話可以保證App進(jìn)入后臺(tái),定時(shí)器不會(huì)暫停,依然繼續(xù)執(zhí)行
[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
經(jīng)驗(yàn)證,后臺(tái)執(zhí)行任務(wù)也將暫停延遲,還是沒(méi)辦法解決App長(zhǎng)時(shí)間進(jìn)入后臺(tái),定時(shí)器暫停問(wèn)題。
我們通過(guò)監(jiān)聽(tīng)mainRunLoop回調(diào)可以發(fā)現(xiàn),當(dāng)App進(jìn)入到后臺(tái),mainRunLoop進(jìn)入了休眠,當(dāng)App回到前臺(tái),mainRunLoop重新喚醒繼續(xù)執(zhí)行。

因此我再想,如果在App進(jìn)入后臺(tái)的時(shí)候,將已經(jīng)睡眠的mainRunLoop重新喚醒,是不是就可以保證定時(shí)器的不暫停,持續(xù)運(yùn)行。
- (void)didEnterBackground {
NSLog(@"--------------------------didEnterBackground");
[[NSRunLoop mainRunLoop] run];
}

經(jīng)驗(yàn)證,結(jié)果如猜想一樣,在App進(jìn)入后臺(tái),重新喚醒mainRunLoop,可以保證定時(shí)器不暫停,可以一直運(yùn)行。
但詭異的事情發(fā)生了,當(dāng)App重新回到前臺(tái),willEnterForeground回調(diào)也沒(méi)走,點(diǎn)擊返回按鍵竟然沒(méi)有響應(yīng)。這究竟為什么呢?
找了一些資料才發(fā)現(xiàn):
[[NSRunLoop mainRunLoop] run];并不是喚醒mainRunLoop,而是會(huì)使得主線程陷入休眠,永遠(yuǎn)等待,但會(huì)讓出主線程時(shí)間片。
[[NSRunLoop mainRunLoop] run]; //主線程永遠(yuǎn)等待,但讓出主線程時(shí)間片
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate distantFuture]]; //等同上面調(diào)用
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate date]]; //立即返回
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10.0]]; //主線程等待,但讓出主線程時(shí)間片,然后過(guò)10秒后返回
[[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate: [NSDate distantFuture]]; //主線程等待,但讓出主線程時(shí)間片;有事件到達(dá)就返回,比如點(diǎn)擊UI等。
[[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate: [NSDate date]]; //立即返回
[[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate: [NSDate dateWithTimeIntervalSinceNow:10.0]]; //主線程等待,但讓出主線程時(shí)間片;有事件到達(dá)就返回,如果沒(méi)有則過(guò)10秒返回。
因此這里我推斷,進(jìn)入后臺(tái)之后,調(diào)用[[NSRunLoop mainRunLoop] run];使得主線程陷入休眠,永遠(yuǎn)等待,導(dǎo)致主線程沒(méi)有去處理定時(shí)器相關(guān)停止操作,因此定時(shí)器才能繼續(xù)執(zhí)行。
那有沒(méi)有辦法可以等回到前臺(tái)的時(shí)候,喚醒主線程的呢?
答案是沒(méi)有,因?yàn)橹骶€程休眠,導(dǎo)致所有前后臺(tái)相關(guān)的通知都不再回調(diào),因此也就沒(méi)法區(qū)分App是否回到前臺(tái),也沒(méi)辦法去喚醒主線程。
那有沒(méi)有方法保證App進(jìn)入后臺(tái)之后,定時(shí)器依然能繼續(xù)執(zhí)行呢?
這個(gè)問(wèn)題我也跟朋友探討了下,正常情況下,App進(jìn)入后臺(tái)之后,如果沒(méi)有申請(qǐng)持續(xù)更新定位、或者語(yǔ)音等權(quán)限,也沒(méi)有通過(guò)UIBackgroundTaskIdentifier向系統(tǒng)借用一段時(shí)間,這時(shí)候正常App進(jìn)入后臺(tái)就會(huì)去中斷其他任務(wù)的執(zhí)行,包括定時(shí)任務(wù),從而掛起,保證系統(tǒng)流暢運(yùn)行。
這也意味著,要先保證App進(jìn)入后臺(tái),定時(shí)器依然能順利執(zhí)行,就要先保證App進(jìn)入后臺(tái)依然存活,而不會(huì)掛起。
而保證App進(jìn)入后臺(tái)的方法,無(wú)非就是勾選并實(shí)現(xiàn)對(duì)應(yīng)的后臺(tái)模式。
除此之外也有一些取巧的方法,如:
-
App退到后臺(tái) - 使用
beginBackgroundTaskWithExpirationHandler向系統(tǒng)申請(qǐng)3分鐘的后臺(tái)任務(wù),此時(shí)backgroundTimeRemaining=180 -
3分鐘限制快到時(shí),啟動(dòng)定位,此時(shí)3分鐘限制被打破,此時(shí)backgroundTimeRemaining=DBL_MAX - 一小段時(shí)間后(如
2s),停止定位,此時(shí)backgroundTimeRemaining≈0 - 重復(fù)步驟2
這些方法都是一些取巧的方式,若非必要,不建議采取。
雖然App進(jìn)入后臺(tái)后,定時(shí)器會(huì)暫停,但是當(dāng)App回到前臺(tái)時(shí),定時(shí)器會(huì)立馬回調(diào)。因此針對(duì)計(jì)時(shí)類的需求,可以在App定時(shí)器啟動(dòng)之前,記錄一個(gè)當(dāng)前系統(tǒng)時(shí)間的時(shí)間戳preTime,當(dāng)定時(shí)器每次回調(diào),就取當(dāng)前系統(tǒng)時(shí)間戳curTime,然后計(jì)算兩個(gè)時(shí)間戳的差值difTime,然后用需要倒計(jì)時(shí)的時(shí)間totalTime,減去時(shí)間戳差值difTime,就可以算出倒計(jì)時(shí)后的時(shí)間,這樣就能優(yōu)雅的解決,倒計(jì)時(shí)進(jìn)入后臺(tái)不準(zhǔn)確的問(wèn)題。
三. 結(jié)論
關(guān)于iOS定時(shí)器進(jìn)入后臺(tái)后在不開(kāi)啟相關(guān)后臺(tái)模式,比如持續(xù)定位更新、音樂(lè)播放等模式下,要想保證定時(shí)器進(jìn)入后臺(tái)能持續(xù)執(zhí)行,我目前并沒(méi)有找到除了一些取巧方法后的好的通用方法。
因此這里對(duì)于一些定時(shí)器倒計(jì)時(shí)進(jìn)入后臺(tái)不準(zhǔn)確問(wèn)題,推薦使用如下方案:
在App定時(shí)器啟動(dòng)之前,記錄一個(gè)當(dāng)前系統(tǒng)時(shí)間的時(shí)間戳preTime,當(dāng)定時(shí)器每次回調(diào),就取當(dāng)前系統(tǒng)時(shí)間戳curTime,然后計(jì)算兩個(gè)時(shí)間戳的差值difTime,然后用需要倒計(jì)時(shí)的時(shí)間totalTime,減去時(shí)間戳差值difTime,就可以算出倒計(jì)時(shí)后的時(shí)間,這樣就能優(yōu)雅的解決,倒計(jì)時(shí)進(jìn)入后臺(tái)不準(zhǔn)確的問(wèn)題。