1、標準寫法
UIBackgroundTaskIdentifier backgroundUpdateTask;
long aa;
NSTimer *_timer;
- (void) didEnterBackground:(NSNotification *)notif{
? ? aa = 0;
? ? [self startTask];
}
- (void) startTask {
? ? _timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(go:) userInfo:nil repeats:YES];
? ? backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
? ? ? ? DDLogInfo(@"bgTask expiration=============");
? ? ? ? [_timer invalidate];
? ? ? ? [[UIApplication sharedApplication] endBackgroundTask:backgroundUpdateTask];
? ? ? ? backgroundUpdateTask = UIBackgroundTaskInvalid;
? ? }];
}
-(void)go:(NSTimer *)tim {
? ? DDLogInfo(@"%@==%ld,%g ",[NSDate date],aa,[UIApplication sharedApplication].backgroundTimeRemaining);
? ? aa++;
? ? if (aa%10 == 0 || [UIApplication sharedApplication].backgroundTimeRemaining == 0) {
? ? ? ? [LocalNotificationManager postLocalNotificationAlertBody:s];
? ? }
}
文檔上說有10分鐘的執(zhí)行時間,但從打印的backgroundTimeRemaining時間來看,只有180秒。
注意:測試此功能不能用Xcode直接debug運行,因為在調(diào)試器鏈接到app的進行的情況下,app是不會在后臺被掛起的,也就是說即使backgroundTimeRemaining =0了,timer里的代碼依然能夠繼續(xù)執(zhí)行。
所以要測試運行態(tài)的情況,要么用文件日志(總是要導(dǎo)出比較麻煩),要么用本地通知來查看。
2、是否能遞歸調(diào)用此方法來持續(xù)獲得執(zhí)行時間
在beginBackgroundTaskWithExpirationHandler里最后再遞歸調(diào)用[self startTask];
經(jīng)嘗試此方法無效,180秒超時后再次申請,會立刻回調(diào)超時的block,并且backgroundTimeRemaining時間一直都是0。
并且由于一直不停的在遞歸創(chuàng)建和終止后臺任務(wù),當(dāng)Expiration真正到來的時候,一個還有一個創(chuàng)建的任務(wù)沒有關(guān)閉。從而導(dǎo)致違背begin和end成對調(diào)用的原則,app被系統(tǒng)強制kill。所以此方法不但不能延長執(zhí)行時間,還會導(dǎo)致app在180秒后臺執(zhí)行時間到達后,被系統(tǒng)kill的情況。
3、beginBackgroundTaskWithExpirationHandler多次被調(diào)用的情況
didEnterBackground每次調(diào)用都會觸發(fā)beginBackgroundTaskWithExpirationHandler來創(chuàng)建新的后臺任務(wù),并用backgroundUpdateTask保存任務(wù)id,但如果第一次的任務(wù)還沒有endBackgroundTask之前,應(yīng)用回到前臺,然后再次進入后臺,就會重新創(chuàng)建一個新的后臺任務(wù),并且backgroundUpdateTask之前保存的id會被覆蓋,這就違背了beginBackgroundTaskWithExpirationHandler與endBackgroundTask成對調(diào)用的原因。因為前一個后臺任務(wù)超時的block回調(diào)的時候,其實是end了后一個taskId對應(yīng)的后臺任務(wù),并且把taskId賦值為UIBackgroundTaskInvalid。而后一個后臺任務(wù)超時的block回調(diào)的時候,taskId已經(jīng)變成了null,對其進行end調(diào)用已經(jīng)無效了,所以相當(dāng)于沒有成對調(diào)用begin和end,導(dǎo)致的結(jié)果就是:后一個后臺任務(wù)超時的時候,app被系統(tǒng)強制kill。
所以每一次創(chuàng)建的后臺任務(wù)都要有一個獨立的變量來維護其taskId,如果只有一個后臺任務(wù),但是有重入的可能,那么應(yīng)該在willEnterForeground回調(diào)中,把前一個后臺任務(wù)進行endBackgroundTask操作,這樣就不存在taskId被覆蓋的問題了?;蛘呤敲看蝑idEnterBackground的時候,檢查taskId == UIBackgroundTaskInvalid,若不滿足該條件,說明taskId已經(jīng)引用了一個正在進行的后臺任務(wù),還沒有完成,由于這個后臺任務(wù)重進前臺又切換回后臺的情況下,backgroundTimeRemaining會被重置為180秒,所以在這種需求下,關(guān)閉前一個任務(wù)再重新建議一個相同的后臺任務(wù)沒有必要,所以應(yīng)該直接
if(backgroundUpdateTask != UIBackgroundTaskInvalid){
? ? return;
}
4、后臺任務(wù)expiration后,app被系統(tǒng)kill的問題
按照文檔里的說法,只要begin與end在真正expiration之前成對調(diào)用,就不會導(dǎo)致系統(tǒng)強制kill app,而是app從后臺執(zhí)行狀態(tài)切換到suspend狀態(tài),但實際測試中,每次expiration之后,app都會被kill掉,根據(jù)是app從launch頁面重新進入。但我在willTerminate通知里的回調(diào)中加了一個local notification,并沒有觸發(fā)這個本地通知。(從app switcher強制退出應(yīng)用的時候會觸發(fā)本地通知,說明本地通知有效)。只能認為是app從后臺狀態(tài)切換到suspend狀態(tài)后,立刻被系統(tǒng)kill掉了,但不知道為什么會這樣。
5、參考另一個文章中的實現(xiàn),可以在任務(wù)結(jié)束后不被kill
參考http://www.cnblogs.com/lyanet/archive/2013/03/26/2983079.html
測試他這個寫法是可以在endTask以后,app變成suspend而不是被直接kill,但我沒找到跟前面寫法有什么本質(zhì)上的區(qū)別。
有三個不同點,依次排除一下。
① 在endTask里面把timer進行了invalidate處理。(測試無關(guān),注釋掉這部分代碼依然可以)
② taskId使用的是屬性而不是全局變量。(測試無關(guān),替換成全局變量依然可以)
③ 使用了application delegate里面的回調(diào),而不是notification center的通知。(把代碼從AppDelegate移動到Controller里面用通知來回調(diào)),竟然也好用。
把controller里的代碼回退到初始狀態(tài)再檢查,還是會被系統(tǒng)kill掉,完全找不到兩者之前有什么不同造成的。
最后,又恢復(fù)了。。感覺什么都沒改,怎么好的完全不知道。
找到原因了!?。?!
懷疑原因是某些其他地方開啟的beginBackgroundTask沒有被對應(yīng)的end掉,找到在引入環(huán)信的時候,要求在ApplicationDelegate里做如下處理:
- (void)applicationDidEnterBackground:(UIApplication *)application {
? ? [[EMClient sharedClient] applicationDidEnterBackground:application];
}
而在ApplicationDelegate里面begin和end正常是因為,用我寫的applicationDidEnterBackground替換到了上面這段。
并且在正常和非正常關(guān)閉的現(xiàn)象做對比,當(dāng)正常調(diào)用endTask的時候,Timer在收到Expiration的時候是會立刻被停止調(diào)用的。而異常的情況下Timer會繼續(xù)調(diào)用直到被系統(tǒng)kill。所以懷疑是環(huán)信引入的代碼沒有做對應(yīng)的begin和end操作。為了驗證這個分析,通過swizzling UIApplication的beginBackgroundTask方法進行測試。
- (UIBackgroundTaskIdentifier )swizzling1 {
? ? UIBackgroundTaskIdentifier taskId = [self swizzling1];
? ? DDLogDebug(@"enter beginBackgroundTaskWithExpirationHandler:%ld",taskId);
? ? return taskId;
}
- (void)swizzling2:(UIBackgroundTaskIdentifier) identifier {
? ? DDLogDebug(@"enter endBackgroundTask:%ld",identifier);
? ? [self swizzling2:identifier];
}
從日志結(jié)果看,begin了3次,id分別為1,4,6(6是我創(chuàng)建的)。end了4次,id分別是6,4,0,0。也就是說環(huán)信內(nèi)部在end的時候不但搞錯了對taskId的引用,很可能是用的同一個變量,創(chuàng)建id=4的時候覆蓋了id=1的,關(guān)閉id=4的時候成功了,并且將id設(shè)置為UIBackgroundTaskInvalid == 0。而對應(yīng)id=1的任務(wù)完成以后,關(guān)閉時執(zhí)行了endTask:0,沒有起到真正關(guān)閉的作用。于是再次等到真正expiration時再次關(guān)閉,依然是endTask:0,最終的結(jié)果就是還是沒有關(guān)閉。到達時限以后begin和end沒有成對調(diào)用,導(dǎo)致app被系統(tǒng)kill掉。
進一步研究,發(fā)現(xiàn)是環(huán)信的初始化中,在hyphenateApplication:didFinishLaunchingWithOptions:中已經(jīng)監(jiān)聽了didEnterBackground和willEnterForeground的事件,并做了后臺任務(wù)的處理,而application的對應(yīng)代理里面再寫一遍就會沖突,看來是文檔不同步造成的問題。