概要
在iOS開(kāi)發(fā)中,Runtime與RunLoop應(yīng)該是iOS Developer技術(shù)進(jìn)階時(shí)需要掌握的兩方面知識(shí),相對(duì)來(lái)說(shuō),它倆也比較接近底層,就現(xiàn)在環(huán)境來(lái)看,面試時(shí)也比較容易問(wèn)到。
關(guān)于這兩項(xiàng),網(wǎng)上的文章大多是講了很多知識(shí)點(diǎn),然而實(shí)際開(kāi)發(fā)中用不到,那就找一個(gè)很簡(jiǎn)單的問(wèn)題來(lái)把這兩項(xiàng)知識(shí)實(shí)戰(zhàn)一下
鎖屏或切換至后臺(tái)時(shí)計(jì)時(shí)器停止
相信大家都遇到過(guò)的問(wèn)題:在注冊(cè)頁(yè)面有一個(gè)NSTimer實(shí)現(xiàn)的驗(yàn)證碼倒計(jì)時(shí)的按鈕,在手機(jī)切出app,把a(bǔ)pp在后臺(tái)掛起時(shí),倒計(jì)時(shí)是停止的,如你切出時(shí)時(shí)間剩余50秒,當(dāng)你從后臺(tái)返回時(shí),倒計(jì)時(shí)依然是50秒。
什么原因?
如果你有關(guān)于RunLoop的知識(shí),你應(yīng)該知道
- 每一個(gè)線程都有一個(gè)自己的RunLoop,他們的關(guān)系是一一對(duì)應(yīng)的
- NSTimer 其實(shí)就是 CFRunLoopTimerRef
上面兩點(diǎn)不懂可以來(lái)這里 -- 深入理解RunLoop
CFRunLoopTimerRef 是基于時(shí)間的觸發(fā)器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一個(gè)時(shí)間長(zhǎng)度和一個(gè)回調(diào)(函數(shù)指針)。當(dāng)其加入到 RunLoop 時(shí),RunLoop會(huì)注冊(cè)對(duì)應(yīng)的時(shí)間點(diǎn),當(dāng)時(shí)間點(diǎn)到時(shí),RunLoop會(huì)被喚醒以執(zhí)行那個(gè)回調(diào)
簡(jiǎn)單來(lái)說(shuō)就是NSTimer在初始化以后,必須被加入到RunLoop中才會(huì)生效
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
使用此方法初始化時(shí),會(huì)自動(dòng)將我們創(chuàng)建的NSTimer加入到RunLoop中
造成NSTimer停止的原因,是當(dāng)app在后臺(tái)掛起時(shí),線程同時(shí)被掛起,RunLoop也就被掛起,而NSTimer是運(yùn)行在RunLoop中的
,所以在app掛起時(shí),NSTimer就同時(shí)停止了工作。
關(guān)于NSTimer想更深入的可以參考iOS開(kāi)發(fā)之 不要告訴我你會(huì)用NSTimer!
怎么解決?
知道了NSTimer停止的原因是因?yàn)榫€程不活躍
那解決NSTimer停止的方法就是app在掛起時(shí),讓其所在RunLoop的線程處于活躍狀態(tài)
我們需要的是UIApplication
的:
- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:(void(^ __nullable)(void))handler
這個(gè)方法,這個(gè)方法就是開(kāi)啟一個(gè)后臺(tái)任務(wù),使線程處于活躍狀態(tài)便于執(zhí)行此后臺(tái)任務(wù),線程活躍了,NSTimer也就可以繼續(xù)跑下去不會(huì)停止,當(dāng)然這個(gè)方法只能讓主線程活躍180秒
,
- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:(void(^ __nullable)(void))handler
與
- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)identifier
是一對(duì),你調(diào)用了前者開(kāi)啟了一個(gè)后臺(tái)任務(wù),就要調(diào)用后者來(lái)結(jié)束這個(gè)任務(wù)
// AppDelegate 中聲明一個(gè)標(biāo)識(shí)
@property (nonatomic, unsafe_unretained) UIBackgroundTaskIdentifier backgroundTaskIdentifier;
然后在app進(jìn)入后臺(tái)時(shí)
- (void)applicationDidEnterBackground:(UIApplication *)application{
// 返回一個(gè)任務(wù)標(biāo)識(shí)
self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void){
// 申請(qǐng)的時(shí)間到期后進(jìn)入這里,即馬上將被掛起,不再活躍
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
// 將標(biāo)識(shí)符標(biāo)記為 UIBackgroundTasksInvalid,任務(wù)結(jié)束
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}];
}
當(dāng)然如果app切回來(lái)的話也要把任務(wù)結(jié)束
- (void)applicationDidBecomeActive:(UIApplication *)application {
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}
到此我們解決了一個(gè)關(guān)于定時(shí)器停止的問(wèn)題,看似解決問(wèn)題的是開(kāi)啟/結(jié)束 后臺(tái)任務(wù)的兩個(gè)方法,但實(shí)際上是我們運(yùn)用了RunLoop的知識(shí)來(lái)解決的,這是RunLoop知識(shí)運(yùn)用在實(shí)際開(kāi)發(fā)中的一個(gè)案例
對(duì)了上述我們用到的方法也可以用于解決程序掛起時(shí)的復(fù)雜操作
比如需要在程序掛起時(shí)向服務(wù)器post一些數(shù)據(jù),以前做的一款產(chǎn)品就是要收集各種操作信息,收集用戶(hù)的操作路徑啊云云,包括用戶(hù)切換至后臺(tái)這樣的操作都要收集。
用這個(gè)方法同樣可以解決,但是要注意:
post要用同步方法,保證在主線程里進(jìn)行