iOS后臺任務beginBackgroundTaskWithExpirationHandler

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的對應代理里面再寫一遍就會沖突,看來是文檔不同步造成的問題。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • IOS開發之----詳解在IOS后臺執行 文一 我從蘋果文檔中得知,一般的應用在進入后臺的時候可以獲取一定時間來...
    dongfang閱讀 1,398評論 0 7
  • 自從古老的iOS4以來,當用戶點擊home建的時候,你可以使你的APP們在內存中處于suspended(掛起)狀態...
    木易林1閱讀 3,215評論 1 4
  • 蘋果官網地址 Background Execution (后臺執行)當用于沒有-啟動應用,系統移到后臺狀態。對于很...
    helinyu閱讀 7,812評論 0 9
  • 文檔app在后臺時會被暫停,暫停的apps會提高電池的使用壽命,并且會讓系統將重要的系統資源投入到引起用戶注意的前...
    zziazm閱讀 4,713評論 0 5
  • 文一 我從蘋果文檔中得知,一般的應用在進入后臺的時候可以獲取一定時間來運行相關任務,也就是說可以在后臺運行一小段時...
    Kloar閱讀 1,515評論 0 1