級別:★☆☆☆☆
標籤:「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+)
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的運行狀態,分別是Not Running、Foreground Inactive、Foreground Active、Forground Inactive、Background、Suspended 6種狀態。
Not Running:指用戶沒有啓動App,或用戶Terminate App 後,App處於的狀態;其餘的五種狀態和不低於iOS13.0的設備端App的運行狀態意義相同。
筆者寫了個定時器,定時輸出「普通定時器進行中」,能夠看到,應用進入後臺後,基本上馬上,就沒有內容輸出了。筆者認爲能夠認爲此時App 已經進入Suspended的狀態。
下邊筆者介紹下,嘗試的App後臺保活方式。
beginBackgroundTaskWithName
和 endBackgroundTask
筆者嘗試過使用相關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.framework 是iOS13新增的framework,筆者認爲此framework中的API能夠在信息流類的App中發揮做用。
系統版本低於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] 定時器運行中
- (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秒。
Xs·H 提到過,若是持續後臺播放無聲音頻或是使用後臺持續定位的方式實現iOS App後臺保活,會浪費電量,浪費CPU,因此通常狀況下,使用這種短期延長App 後臺保活的方式,應該夠開發者作須要的操做了。
對於音視頻類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];
複製代碼
- (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];
}
複製代碼
- (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__);
}
複製代碼
開啓後臺定位持續更新配置,添加了位置隱私申請後,在應用使用持續定位的狀況下,能夠實現後臺保活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()
當須要實現下載資源類的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];
複製代碼
若是咱們的App是信息流類App,那麼咱們可能會使用到BGTaskScheduler.framework中的API,實現後臺保活App,幫助用戶較早地獲取到較新信息。
筆者嘗試使用BGTaskScheduler 作了一個獲取到App調度的時候。更新首頁按鈕顏色爲隨機色而且記錄調度時間的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 刷新。
- (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,輸入以下內容
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier: @"com.qishare.ios.wyw.background.refresh"]
複製代碼
-> 再次點擊Continue program execution,就能夠模擬後臺啓動任務,調用咱們的App。
以前記得聽沐靈洛提過怎麼便於查看日誌,正好我這裏也用到了。便於咱們能夠直接在File App中查看寫入到咱們App的Documents中的文件,能夠在Info.plist文件中添加key爲LSSupportsOpeningDocumentsInPlace ,value爲YES的鍵值對App 接入 iOS 11 的 Files App。
通過咱們操做後,就能夠打開File App -> 瀏覽 -> 個人iPhone -> 查看選擇咱們的App -> 查看咱們的日誌記錄文件。
小編微信:可加並拉入《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的啓動流程
奇舞週刊