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