iOS App後臺保活

級別:★☆☆☆☆
標籤:「iOS App 後臺保活」「BackgroundTasks」「後臺下載資源」
做者: WYW
審校: QiShare團隊php


前段時間,筆者和GY哥一塊兒吃飯聊天的時候,GY哥問了筆者一個問題,iOS App 能夠後臺保活嗎?是如何作到後臺保活的?當時筆者只想到了能夠在後臺播放靜音的音樂,對於喚醒App,能夠考慮使用推送的方式。GY哥提到播放靜音文件會影響上線嗎?這我就不大知道了…...因爲沒有相關實踐,筆者後來在網上查了相關資料,總結了本文。html

筆者查詢了相關資料後發現,iOS App能夠實現後臺保活。ios

短期保活的方式有beginBackgroundTaskWithName;git

App長時間保活的方式有:播放無聲音樂、後臺持續定位、後臺下載資源、BGTaskScheduler等;github

喚醒App的方式有:推送、VoIP等;web

本文分爲以下幾部分:數組

  • App 運行狀態、及狀態變化微信

  • App 後臺保活方式簡介網絡

  • 短期App後臺保活session

  • Background Modes AVAudio,AirPlay,and Picture in Picture

  • Background Modes Location updates

  • BGTaskScheduler (iOS13.0+)

App 運行狀態、及狀態變化

不低於iOS13.0的設備端App 運行狀態

不低於iOS13.0設備端App 運行狀態

iOS13.0+的設備,支持多場景,共有上圖中的Unattached、Foreground Inactive、Foreground Active、Forground Inactive、Background、Suspended 6種狀態。

Unattached:多個場景的狀況,若是建立的場景不是當前顯示的場景,那麼場景處於Unattached狀態;

Foreground Inactive:應用啓動後,顯示啓動圖的過程當中,處於Foreground Inactive狀態;

Forground Active:應用啓動後,顯示出來咱們設置的rootViewController以後,場景處於Forground Active;

Foreground Inactive:應用啓動後,場景處於顯示狀態,數據加載完畢,且用戶和App沒有交互過程當中,處於Forground Inactive狀態;

Background:用戶點擊Home鍵、或者是切換App後、鎖屏後,應用進入Background狀態;

Suspended:進入Background後,應用的代碼不執行後,應用進入Suspended狀態;(代碼是否在運行,能夠在應用中寫定時器,定時輸出內容,從Xcode控制檯,或Mac端控制檯查看是否有輸出內容來判斷)

低於iOS13.0的設備端App 運行狀態

低於iOS13.0設備端App 運行狀態

上圖是低於iOS13.0的設備端App的運行狀態,分別是Not Running、Foreground Inactive、Foreground Active、Forground Inactive、Background、Suspended 6種狀態。

Not Running:指用戶沒有啓動App,或用戶Terminate App 後,App處於的狀態;其餘的五種狀態和不低於iOS13.0的設備端App的運行狀態意義相同。

App 進入後臺狀態變化

筆者寫了個定時器,定時輸出「普通定時器進行中」,能夠看到,應用進入後臺後,基本上馬上,就沒有內容輸出了。筆者認爲能夠認爲此時App 已經進入Suspended的狀態。

App 進入後臺

下邊筆者介紹下,嘗試的App後臺保活方式。

iOS App 後臺保活方式簡介

短期App後臺保活

beginBackgroundTaskWithNameendBackgroundTask

筆者嘗試過使用相關API,測試過2款手機。

對於系統版本低於iOS13(iOS 12.3)的設備(iPhone6 Plus)後臺運行時間約3分鐘(175秒);

對於系統版本不低於iOS13(iOS 13.0)的設備(iPhone6 Plus)後臺運行時間約31秒;

播放無聲音樂

App 進入後臺後,播放無聲音樂,適用於音視頻類App。

筆者對逆向不瞭解,從iOS項目技術還債之路《一》後臺下載趟坑中得知,騰訊視頻、愛奇藝採用了播放無聲音樂保活的方式。

後臺持續定位

對於定位類App,持續定位App,能夠實現App後臺保活。定位類App須要後臺保活,像系統的地圖應用,在導航的時候切換App的時候,就須要後臺保活。

後臺下載資源

對於須要下載資源的App,須要後臺下載資源,如咱們在某App下載資源的時候,咱們但願在切換App時候,或者App退出後臺後,資源仍然繼續下載,這樣當咱們打開App的時候,資源已經下載好了。

BackgroundTasks

BackgroundTasks.framework 是iOS13新增的framework,筆者認爲此framework中的API能夠在信息流類的App中發揮做用。

短期App後臺保活

系統版本低於iOS13.0的設備

系統版本低於iOS13.0的設備,在應用進入後臺的時候,開始後臺任務([[UIApplication sharedApplication] beginBackgroundTaskWithName:)。在應用進入前臺時或後臺任務快過時的回調中,終止後臺任務([[UIApplication sharedApplication] endBackgroundTask:)。

示例代碼以下:

- (void)applicationDidEnterBackground:(UIApplication *)application {
    
    self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithName:kBgTaskName expirationHandler:^{
       if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
           [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
           self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
       }
    }];
}
複製代碼
- (void)applicationWillEnterForeground:(UIApplication *)application {
    
    [[UIApplication sharedApplication] endBackgroundTask: self.backgroundTaskIdentifier];
}
複製代碼

添加相關代碼後,筆者在iOS12.4的6 Plus上測試結果以下,應用在進入後臺後,大概還運行了175秒。

2019-12-29 19:06:55.647288+0800 QiAppRunInBackground[1481:409744] -[AppDelegate applicationDidEnterBackground:]:應用進入後臺DidEnterBackground

2019-12-29 19:06:56.256877+0800 QiAppRunInBackground[1481:409744] 定時器運行中

….

2019-12-29 19:09:50.812460+0800 QiAppRunInBackground[1481:409744] 定時器運行中

系統版本不低於iOS13.0的設備

- (void)sceneDidEnterBackground:(UIScene *)scene  API_AVAILABLE(ios(13.0)){
   
    self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithName:kBgTaskName expirationHandler:^{
        if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
            [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
            self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
        }
    }];
}
複製代碼
- (void)sceneWillEnterForeground:(UIScene *)scene  API_AVAILABLE(ios(13.0)){
    
    [[UIApplication sharedApplication] endBackgroundTask: self.backgroundTaskIdentifier];
}
複製代碼

添加相關代碼後,筆者在iOS13.0的6s上測試結果以下,應用在進入後臺後,大概還運行了31秒。

iOS13.0+ App 進入後臺

Xs·H 提到過,若是持續後臺播放無聲音頻或是使用後臺持續定位的方式實現iOS App後臺保活,會浪費電量,浪費CPU,因此通常狀況下,使用這種短期延長App 後臺保活的方式,應該夠開發者作須要的操做了。

Background Modes AVAudio,AirPlay,and Picture in Picture

對於音視頻類App,若是須要後臺保活App,在App 進入後臺後,能夠考慮先使用短期保活App的方式,若是後臺保活App方式快結束後,還沒處理事情,那麼能夠考慮使用後臺播放無聲音樂。相關示例代碼以下。

- (AVAudioPlayer *)player {
    
    if (!_player) {
        NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"SomethingJustLikeThis" withExtension:@"mp3"];
        AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:nil];
        audioPlayer.numberOfLoops = NSUIntegerMax;
        _player = audioPlayer;
    }
    return _player;
}
複製代碼
[self.player prepareToPlay];
複製代碼

系統版本低於iOS13.0的設備

- (void)applicationDidEnterBackground:(UIApplication *)application {

    NSLog(@"%s:應用進入後臺DidEnterBackground", __FUNCTION__);
    self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithName:kBgTaskName expirationHandler:^{

       if ([QiAudioPlayer sharedInstance].needRunInBackground) {
           [[QiAudioPlayer sharedInstance].player play];
       }
       if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
           [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
           self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
       }
    }];
}
複製代碼
- (void)applicationWillEnterForeground:(UIApplication *)application {

    NSLog(@"%s:應用將進入前臺WillEnterForeground", __FUNCTION__);
    if ([QiAudioPlayer sharedInstance].needRunInBackground) {
        [[QiAudioPlayer sharedInstance].player pause];
    }
    [[UIApplication sharedApplication] endBackgroundTask: self.backgroundTaskIdentifier];
}
複製代碼

系統版本不低於iOS13.0的設備

- (void)sceneDidEnterBackground:(UIScene *)scene  API_AVAILABLE(ios(13.0)){

    NSLog(@"%s:應用已進入後臺DidEnterBackground", __FUNCTION__);

    self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithName:kBgTaskName expirationHandler:^{
        if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
            if ([QiAudioPlayer sharedInstance].needRunInBackground) {
                [[QiAudioPlayer sharedInstance].player play];
            }
            NSLog(@"終止後臺任務");
            [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
            self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
        }
    }];
}
複製代碼
- (void)sceneWillEnterForeground:(UIScene *)scene  API_AVAILABLE(ios(13.0)){

    if ([QiAudioPlayer sharedInstance].needRunInBackground) {
        [[QiAudioPlayer sharedInstance].player pause];
    }
    [[UIApplication sharedApplication] endBackgroundTask: self.backgroundTaskIdentifier];
    NSLog(@"%s:應用將進入前臺WillEnterForeground", __FUNCTION__);
}
複製代碼

Background Modes Location updates

開啓後臺定位持續更新配置,添加了位置隱私申請後,在應用使用持續定位的狀況下,能夠實現後臺保活App。

添加後臺獲取位置及音頻使用能力

添加獲取位置隱私申請

對於定位類App,若是須要後臺保活App,在用戶使用了定位功能後,App 進入後臺後,App自動具有後臺保活能力,部分示例代碼以下。

self.locationManager = [CLLocationManager new];
    self.locationManager.delegate = self;
    [self.locationManager requestAlwaysAuthorization];
    @try {
       self.locationManager.allowsBackgroundLocationUpdates = YES;
    } @catch (NSException *exception) {
        NSLog(@"異常:%@", exception);
    } @finally {
        
    }
    [self.locationManager startUpdatingLocation];
複製代碼

若是遇到以下異常信息:

2019-12-29 19:57:46.481218+0800 QiAppRunInBackground[1218:141397] 異常:Invalid parameter not satisfying: !stayUp || CLClientIsBackgroundable(internal->fClient) || _CFMZEnabled()

  • 檢查:Signing&Capablities 的 backgounrd Modes 中 Location updates是否勾選;

後臺下載資源

當須要實現下載資源類的App在進入後臺後,持續下載資源的需求時。咱們可能須要使用後臺以下示例示例代碼。

建立指定標識的後臺NSURLSessionConfiguration,配置好

NSURL *url = [NSURL URLWithString:@"https://images.pexels.com/photos/3225517/pexels-photo-3225517.jpeg"];
    NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.qishare.ios.wyw.backgroundDownloadTask"];
// 低於iOS13.0設備資源下載完後 能夠獲得通知 AppDelegate.m 文件中的 - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler
// iOS13.0+的設備資源下載完後 直接在下載結束的代理方法中會有回調
    sessionConfig.sessionSendsLaunchEvents = YES;
// 當傳輸大數據量數據的時候,建議將此屬性設置爲YES,這樣系統能夠安排對設備而言最佳的傳輸時間。例如,系統可能會延遲傳輸大文件,直到設備鏈接充電器並經過Wi-Fi鏈接到網絡爲止。 此屬性的默認值爲NO。
    sessionConfig.discretionary = YES;
    NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
    NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:url];
    [downloadTask resume];
複製代碼

BGTaskScheduler(iOS13.0+)

若是咱們的App是信息流類App,那麼咱們可能會使用到BGTaskScheduler.framework中的API,實現後臺保活App,幫助用戶較早地獲取到較新信息。

筆者嘗試使用BGTaskScheduler 作了一個獲取到App調度的時候。更新首頁按鈕顏色爲隨機色而且記錄調度時間的Demo。

Demo示意圖

項目配置

爲了App 支持 BGTaskScheduler,須要在項目中配置Background fetch,及Background Processing;

須要在Info.plist文件中添加 key 爲Permitted background task scheduler identifiers,Value爲數組的內容。

Value的數組填寫,刷新的任務標識和清理的任務標識。

註冊後臺任務

在應用啓動後,註冊後臺任務。

- (void)registerBgTask {
    
    if (@available(iOS 13.0, *)) {
        BOOL registerFlag = [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:kRefreshTaskId usingQueue:nil launchHandler:^(__kindof BGTask * _Nonnull task) {
            [self handleAppRefresh:task];
        }];
        if (registerFlag) {
            NSLog(@"註冊成功");
        } else {
            NSLog(@"註冊失敗");
        }
    } else {
        // Fallback on earlier versions
    }
    
    if (@available(iOS 13.0, *)) {
        [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:kCleanTaskId usingQueue:nil launchHandler:^(__kindof BGTask * _Nonnull task) {
            [self handleAppRefresh:task];
        }];
    } else {
        // Fallback on earlier versions
    }
}
複製代碼

調度App 刷新

應用進入後臺後,調度App 刷新。

- (void)sceneDidEnterBackground:(UIScene *)scene  API_AVAILABLE(ios(13.0)){

    [self scheduleAppRefresh];
}


- (void)scheduleAppRefresh {
    
    if (@available(iOS 13.0, *)) {
        BGAppRefreshTaskRequest *request = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:kRefreshTaskId];
        // 最先15分鐘後啓動後臺任務請求
        request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:15.0 * 60];
        NSError *error = nil;
        [[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error];
        if (error) {
            NSLog(@"錯誤信息:%@", error);
        }
        
    } else {
        // Fallback on earlier versions
    }
}
複製代碼

獲得後臺任務調度的時候,調用App刷新的方法,筆者在這個方法中作了發送更新首頁按鈕顏色的通知,而且記錄了當前更新時間的記錄。

- (void)handleAppRefresh:(BGAppRefreshTask *)appRefreshTask  API_AVAILABLE(ios(13.0)){
    
    [self scheduleAppRefresh];
    
    NSLog(@"App刷新====================================================================");
    NSOperationQueue *queue = [NSOperationQueue new];
    queue.maxConcurrentOperationCount = 1;
    
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        
        [[NSNotificationCenter defaultCenter] postNotificationName:AppViewControllerRefreshNotificationName object:nil];
        
        NSLog(@"操做");
        NSDate *date = [NSDate date];
        NSDateFormatter *dateFormatter = [NSDateFormatter new];
        [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm"];
        NSString *timeString = [dateFormatter stringFromDate:date];
        
        NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"QiLog.txt"];
        if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
            NSData *data = [timeString dataUsingEncoding:NSUTF8StringEncoding];
            [[NSFileManager defaultManager] createFileAtPath:filePath contents:data attributes:nil];
        } else {
            NSData *data = [[NSData alloc] initWithContentsOfFile:filePath];
            NSString *originalContent = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            NSString *content = [originalContent stringByAppendingString:[NSString stringWithFormat:@"\n時間:%@\n", timeString]];
            data = [content dataUsingEncoding:NSUTF8StringEncoding];
            [data writeToFile:filePath atomically:YES];
        }
    }];
    
    appRefreshTask.expirationHandler = ^{
        [queue cancelAllOperations];
    };
    [queue addOperation:operation];
    
    __weak NSBlockOperation *weakOperation = operation;
    operation.completionBlock = ^{
        [appRefreshTask setTaskCompletedWithSuccess:!weakOperation.isCancelled];
    };
}
複製代碼

通過測試,發現App 在退到後臺,沒有手動Terminate App的狀況下。蘋果有調用過App調度任務的方法。現象上來看就是隔一段時間,咱們再打開App 的時候能夠發現,首頁的按鈕顏色改變了,相應的日誌中追加了,調起相關方法的時間記錄。

手動觸發後臺任務調度

Xcode運行咱們的App

-> App 退到後臺

-> 打開App 進入前臺

-> 點擊下圖中藍框中的Pause program execution,輸入以下內容

後臺模擬調起App

e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier: @"com.qishare.ios.wyw.background.refresh"]
複製代碼

-> 再次點擊Continue program execution,就能夠模擬後臺啓動任務,調用咱們的App。

Continue program execution

查看日誌記錄小提示

以前記得聽沐靈洛提過怎麼便於查看日誌,正好我這裏也用到了。便於咱們能夠直接在File App中查看寫入到咱們App的Documents中的文件,能夠在Info.plist文件中添加key爲LSSupportsOpeningDocumentsInPlace ,value爲YES的鍵值對App 接入 iOS 11 的 Files App

通過咱們操做後,就能夠打開File App -> 瀏覽 -> 個人iPhone -> 查看選擇咱們的App -> 查看咱們的日誌記錄文件。

示例Demo

QiAppRunInBackground

參考學習網址

小編微信:可加並拉入《QiShare技術交流羣》。

關注咱們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公衆號)

推薦文章:
Swift 中使用 CGAffineTransform
iOS 性能監控(一)—— CPU功耗監控
iOS 性能監控(二)—— 主線程卡頓監控
iOS 性能監控(三)—— 方法耗時監控
初識Flutter web
用SwiftUI給視圖添加動畫
用SwiftUI寫一個簡單頁面
iOS App啓動優化(三)—— 本身作一個工具監控App的啓動耗時
iOS App啓動優化(二)—— 使用「Time Profiler」工具監控App的啓動耗時
iOS App啓動優化(一)—— 瞭解App的啓動流程
奇舞週刊

相關文章
相關標籤/搜索