版本記錄
| 版本號 | 時間 |
|---|---|
| V1.0 | 2022.11.06 星期日 |
前言
Background Modes我們在程序中總會用到,包括語音、定位更新、后臺任務(wù)以及遠(yuǎn)程通知等,這個模塊我們就一起來學(xué)習(xí)下。
開始
Background Modes是我們常用的模式,比如語音、定位更新、后臺任務(wù)以及遠(yuǎn)程通知等。Xcode里的后臺模式如下所示:

在本教程中,您將創(chuàng)建一個使用音頻播放、位置更新、關(guān)鍵任務(wù)和后臺拉取的應(yīng)用程序,以了解最常見的后臺模式。本文來自翻譯。
2010年,隨著iOS 4的發(fā)布,蘋果開始允許應(yīng)用程序在后臺工作,并從那時起不斷發(fā)展和改進(jìn)后臺模式。iOS限制使用后臺操作來改善用戶體驗和延長電池壽命。你的應(yīng)用可以在后臺運行特定的情況,包括:播放音頻,更新位置和從服務(wù)器獲取最新的內(nèi)容。
如果你的任務(wù)不屬于允許的類別,后臺模式可能不適合你。如果你試圖使用超出其作用范圍的后臺模式來操縱系統(tǒng),你可能會面臨App Store的拒絕。
在本后臺模式教程中,你將了解你的應(yīng)用程序可以在后臺做的四件事:
- Play audio - 播放音頻:允許應(yīng)用程序在后臺繼續(xù)播放音頻。
- Receive location updates - 接收位置更新:允許應(yīng)用程序在后臺接收位置更改。
- Complete finite-length critical tasks - 完成有限長度的關(guān)鍵任務(wù):允許應(yīng)用程序在移動到后臺后繼續(xù)完成關(guān)鍵任務(wù)。
- Background Fetch - 后臺獲取:在iOS調(diào)度的時間表上執(zhí)行后臺更新。
在深入研究之前,我們先來快速瀏覽一下iOS的基本后臺模式:
- Audio, AirPlay, and Picture in Picture - 音頻,AirPlay,和圖片中的圖片:當(dāng)應(yīng)用程序在后臺時播放音頻和視頻。
- Location Updates - 位置更新:在后臺時繼續(xù)接收位置更新。
- Voice over IP - IP語音:通過因特網(wǎng)發(fā)送和接收語音。
-
External accessory communication - 外部配件通信:通過
lightning接口與外部配件通信。 -
Using Bluetooth LE accessories - 使用藍(lán)牙LE配件:在后臺與藍(lán)牙
LE配件通信。 -
Acting as a Bluetooth LE accessory - 充當(dāng)藍(lán)牙LE配件:允許應(yīng)用程序為配件提供藍(lán)牙
LE信息。 - Background fetch - 后臺拉取:執(zhí)行數(shù)據(jù)刷新。
- Remote notitifications - 遠(yuǎn)程通知:發(fā)送和接收遠(yuǎn)程通知。
- Background processing - 后臺處理:執(zhí)行較長的關(guān)鍵進(jìn)程。
您將向示例應(yīng)用程序添加上述模式中的四種——音頻、定位、后臺處理和后臺獲取(audio, location, background processing and background fetches)。如果你只對其中的一些模式感興趣,可以隨意跳過,只玩你感興趣的模式。
注意:要獲得完整的效果,您應(yīng)該在真實的設(shè)備上進(jìn)行操作。在模擬器中,當(dāng)你忘記一個步驟時,應(yīng)用程序可能會在后臺運行。然后當(dāng)你切換到真正的設(shè)備時,它可能根本無法工作。
在您可以在物理設(shè)備上運行項目之前,您必須設(shè)置您的development team,如下所示:

構(gòu)建并運行示例項目來感受一下Sleepless,這是一個從不休息的應(yīng)用程序,因為它在后臺做事情。有四個tab —— 每個覆蓋一個模式:

您要添加的第一個capability是background audio。
Playing Audio
在物理設(shè)備上構(gòu)建并運行Sleepless。導(dǎo)航到audio tab,播放音樂,然后通過返回主屏幕把應(yīng)用程序放在后臺。音樂將停止播放。
打開 AudioModel.swift
該應(yīng)用程序利用AVQueuePlayer對歌曲進(jìn)行排隊并按順序播放。模型觀察播放器的currentItem值以提供視圖的更新。
1. Giving Credit Where Credit Is Due
最初的項目包括來自incompetech.com的音頻文件,這是一個流行的免版稅音樂網(wǎng)站。你可以免費使用帶有版權(quán)的音樂。這三首歌都是Kevin MacLeod寫的:
“Feelin Good” Kevin MacLeod (incompetech.com)
Licensed under Creative Commons: By Attribution 3.0 License
http://creativecommons.org/licenses/by/3.0/“Iron Bacon” Kevin MacLeod (incompetech.com)
Licensed under Creative Commons: By Attribution 3.0 License
http://creativecommons.org/licenses/by/3.0/“What You Want” Kevin MacLeod (incompetech.com)
Licensed under Creative Commons: By Attribution 3.0 License
http://creativecommons.org/licenses/by/3.0/
謝謝你美妙的音樂,Kevin!
注意:在蘋果的UIKit文檔中查看Execution States for Apps,了解更多關(guān)于
active state和其他的信息。
2. Testing Audio in the Background
為什么當(dāng)應(yīng)用程序進(jìn)入后臺時音樂停止了?好吧,缺了一個關(guān)鍵的部分!
大多數(shù)后臺模式都不能工作,除非你啟用特定的功能,表明應(yīng)用程序想要在后臺運行代碼。特例是關(guān)鍵任務(wù)完成,任何應(yīng)用程序都可以執(zhí)行。
當(dāng)激活時,音頻后臺模式告訴iOS繼續(xù)播放音頻,即使應(yīng)用程序在后臺。沒錯,音頻后臺模式實際上是自動的。你只需要激活它。
返回Xcode,執(zhí)行以下操作:


接下來,雙擊Background Modes以添加此功能。展開Background Modes功能,然后勾選Audio, AirPlay, and Picture in Picture以啟用background audio。

在物理設(shè)備上構(gòu)建并運行應(yīng)用程序。像以前一樣啟動音樂,然后離開應(yīng)用程序。這一次音頻將繼續(xù)。就這么簡單!

接下來,你將使用Location updates后臺模式繼續(xù)接收位置更新,即使應(yīng)用程序是在后臺。
Receiving Location Updates
首先,構(gòu)建并運行應(yīng)用程序。選擇Location tab并點擊Start。什么也沒有發(fā)生,因為你錯過了一些重要的步驟。你現(xiàn)在要改變了。
1. Enabling Location Updates
打開LocationModel.swift。這是為LocationView提供位置數(shù)據(jù)的代碼。您將對init()做一個簡單的更改。替換以下兩行:
mgr.requestWhenInUseAuthorization()
mgr.allowsBackgroundLocationUpdates = false
為
mgr.requestAlwaysAuthorization()
mgr.allowsBackgroundLocationUpdates = true
第一行請求位置更新,即使應(yīng)用程序沒有在使用。第二個請求甚至在后臺進(jìn)行更新。
回到Signing & Capabilities界面,勾選Location updates框,讓iOS知道你的應(yīng)用程序想在后臺接收位置更新。

除了勾選這個框,iOS還要求你在Info.plist中設(shè)置一個鍵向用戶解釋為什么你需要后臺更新。如果不包含這一點,位置請求將會無聲地失敗。
打開Info.plist。并添加Privacy — Location Always and When In Use Usage Description和Privacy — Location When In Use Usage Description的鍵。然后輸入The app will show your location on a map作為兩個鍵的value。

現(xiàn)在,構(gòu)建并運行,切換到Location tab,點擊Start。
當(dāng)它第一次加載時,你會看到你寫進(jìn)你的位置隱私原因的消息。
點擊Allow while using app,在外面或大樓周圍散步——盡量不要因為抓口袋妖怪而分心。

位置更新應(yīng)該開始出現(xiàn)。如果沒有,將應(yīng)用再次發(fā)送到后臺,以觸發(fā)Always提示進(jìn)行位置跟蹤。你也可以使用Settings應(yīng)用程序,在Privacy ? Location Services ? Sleepless設(shè)置中啟用Sleepless應(yīng)用程序始終跟蹤。
如果你將應(yīng)用程序發(fā)送到后臺,你仍然會看到控制臺中發(fā)生的位置更新。
一段時間后,你應(yīng)該會看到如下內(nèi)容:

2. Testing Location Mode in the Background
如果你退出應(yīng)用程序,你應(yīng)該看到應(yīng)用程序更新了控制臺日志中的位置。再次打開它,可以看到地圖上所有的大頭針,顯示你在步行過程中去過的地方。
如果你正在使用模擬器,你也可以使用它來模擬移動!點擊Features ? Location菜單:

非常簡單,對吧?打開第三個選項卡和第三個后臺模式!
Completing Critical Tasks Upon Moving to the Background
下一個后臺模式的正式名稱是Extending Your App’s Background Execution Time,任務(wù)完成說起來容易一點!
從技術(shù)上講,這根本不是后臺模式。你不需要在Capabilities中聲明你的應(yīng)用程序使用它。它是一個API,當(dāng)你的應(yīng)用程序在后臺時,允許你在有限的時間內(nèi)運行任意代碼,給你更多的時間來完成關(guān)鍵任務(wù),如保存數(shù)據(jù)。
1. When to Use Task Completion
Completion后臺模式的一個有效用例是完成一些關(guān)鍵任務(wù),例如保存用戶的輸入或發(fā)布一個事務(wù)。有很多可能性。
由于代碼是任意的,你可以使用這個API做幾乎任何事情:執(zhí)行冗長的計算,對圖像應(yīng)用過濾器,渲染一個復(fù)雜的3D網(wǎng)格 —— 任何!你的想象力是極限,只要你記住你只有一些時間,而不是無限的時間。稍后,您將設(shè)置一個在后臺運行的冗長計算,因此您可以看到這個API是如何工作的。
iOS決定了你的應(yīng)用程序移到后臺后的時間。你被授予的時間沒有保證,但你總是可以檢查UIApplication.shared.backgroundTimeRemaining。這會告訴你還剩下多少時間。
一般的,基于觀察的共識是你大約有30秒。同樣,沒有保證,API文檔甚至沒有給出一個估計——所以不要依賴這個數(shù)字。你可能有5分鐘或5秒鐘的時間,所以你的應(yīng)用程序需要為中斷做好準(zhǔn)備。當(dāng)你的時間快到的時候,iOS會給你回調(diào)信號。
2. Setting Up a Completion Task
這里有一個每個計算機科學(xué)專業(yè)的學(xué)生都應(yīng)該熟悉的常見任務(wù):計算 Fibonacci Sequence中的數(shù)字。這里的扭轉(zhuǎn)是,你將應(yīng)用程序移動到后臺后計算這些數(shù)字。
打開CompleteTaskModel.swift,看看已經(jīng)有什么了。按照目前的情況,該視圖將按順序計算斐波那契數(shù)列并顯示結(jié)果。
如果你現(xiàn)在掛起一個實際設(shè)備上的應(yīng)用程序,計算將停止,并在應(yīng)用程序再次激活時恢復(fù)到原來的位置。你的任務(wù)是創(chuàng)建一個后臺任務(wù),這樣計算就可以一直運行,直到iOS說“時間到!”
你首先需要添加以下內(nèi)容到CompleteTaskModel:
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
此屬性標(biāo)識要在后臺運行的任務(wù)請求。
接下來,在resetcalculation()之前向CompleteTaskModel添加以下方法:
func registerBackgroundTask() {
backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
print("iOS has signaled time has expired")
self?.endBackgroundTaskIfActive()
}
}
registerBackgroundTask()告訴iOS,當(dāng)應(yīng)用移動到后臺時,你需要更多的時間來完成你正在做的事情。返回的值是這個任務(wù)的標(biāo)識符,這樣你就可以告訴iOS你什么時候完成了。在這個調(diào)用之后,如果你的應(yīng)用程序移動到后臺,它仍然會得到CPU時間,直到你調(diào)用endBackgroundTask(_:)。
好吧,至少有一些CPU時間。
3. Ending the Completion Task
如果你在后臺一段時間后沒有調(diào)用endBackgroundTask(_:), iOS將調(diào)用當(dāng)你調(diào)用beginBackgroundTask(expirationHandler:)時定義的閉包。這使您有機會停止執(zhí)行代碼。
因此,調(diào)用endBackgroundTask(_:)來告訴系統(tǒng)您已經(jīng)完成是一個好主意。如果你不調(diào)用它并在這個塊運行后繼續(xù)執(zhí)行代碼,iOS將終止你的應(yīng)用程序!
將這個方法添加到registerBackgroundTask()下面:
func endBackgroundTaskIfActive() {
let isBackgroundTaskActive = backgroundTask != .invalid
if isBackgroundTaskActive {
print("Background task ended.")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
}
這將結(jié)束后臺任務(wù),如果它是主動注冊的,并將其ID重置為invalid。
4. Registering and Ending Background Tasks
現(xiàn)在,對于重要的部分:更新onChangeOfScenePhase(_:)來注冊和結(jié)束后臺任務(wù),這取決于應(yīng)用程序是移動到后臺還是活動狀態(tài)。
用以下語句替換這兩個case語句:
case .background:
let isTimerRunning = updateTimer != nil
let isTaskUnregistered = backgroundTask == .invalid
if isTimerRunning && isTaskUnregistered {
registerBackgroundTask()
}
case .active:
endBackgroundTaskIfActive()
當(dāng)切換到后臺background狀態(tài)時,這將在任務(wù)正在運行但未注冊時注冊它。當(dāng)切換到活動active狀態(tài)時,它將結(jié)束后臺任務(wù)。
在beginPauseTask()中,在updateTimer = nil之后添加這一行:
endBackgroundTaskIfActive()
現(xiàn)在,當(dāng)用戶停止計算時,你調(diào)用endBackgroundTask(_:)來告訴iOS你不需要任何額外的CPU時間。
注意:每次調(diào)用
beginBackgroundTask(expirationHandler:)時調(diào)用endBackgroundTask(_:)是很重要的。如果你調(diào)用beginBackgroundTask(expirationHandler:)兩次,并且只對其中一個任務(wù)調(diào)用endBackgroundTask(_:),你仍然會得到CPU時間,直到你使用第二個后臺任務(wù)的標(biāo)識符第二次調(diào)用endBackgroundTask(_:)。
構(gòu)建并運行,然后切換到第三個選項卡。

點擊Play并觀看應(yīng)用程序計算這些甜蜜的斐波那契值。將應(yīng)用發(fā)送到后臺,但要觀察Xcode控制臺的輸出。當(dāng)剩下的時間減少時,你的應(yīng)用程序應(yīng)該繼續(xù)更新數(shù)字。
在大多數(shù)情況下,這個時間從30秒開始,一直到5秒。如果你在達(dá)到5秒時等待時間過期——或者你看到的任何值——iOS就會調(diào)用過期block。
你的應(yīng)用程序應(yīng)該很快停止產(chǎn)生輸出。然后,如果你回到應(yīng)用程序,計時器應(yīng)該會再次啟動,斐波那契瘋狂將繼續(xù)。
在前臺和后臺之間切換,看看如何通過每次切換獲得額外的時間塊。
下面是本教程的最后一個主題:background fetch
Background Fetch
Background fetch是在iOS 7中引入的。它可以讓你的應(yīng)用程序顯示最新的同時最小化對電池壽命的影響。從iOS 13開始,蘋果引入了一個新的后臺任務(wù)調(diào)度程序API,提供了顯著的改進(jìn)。
例如,假設(shè)你正在應(yīng)用程序中實現(xiàn)一個新聞feed。在后臺獲取之前,你將在應(yīng)用程序每次啟動時刷新feed。
遺憾的是,當(dāng)刷新時,用戶會看到幾秒鐘的舊標(biāo)題。你知道,有些人會試圖挖掘一個故事,結(jié)果卻發(fā)現(xiàn)它消失了,取而代之的是一個不相關(guān)的故事??雌饋聿惶谩?/p>
如果當(dāng)用戶打開你的應(yīng)用時,最新的標(biāo)題就會神奇地出現(xiàn)在那里,不是更好嗎?這是后臺獲取給你的能力。
當(dāng)啟用時,系統(tǒng)利用使用模式來確定何時觸發(fā)后臺獲取。例如,如果用戶在上午9點打開你的新聞應(yīng)用,background fetch可能會在上午9點之前發(fā)生。系統(tǒng)決定發(fā)出background fetch的最佳時間,由于這個原因,它不適合進(jìn)行關(guān)鍵更新。
1. Understanding Background Fetch
Background fetch由BGTaskScheduler控制,這是一個復(fù)雜的系統(tǒng),用于平衡所有影響用戶體驗的因素,如性能、使用模式、電池壽命等。
Background fetch通常涉及從外部來源(如網(wǎng)絡(luò)服務(wù))獲取信息。在本后臺模式教程中,您將獲取當(dāng)前時間,而不使用網(wǎng)絡(luò)。
為了實現(xiàn)background fetch,你需要完成這些任務(wù)——但現(xiàn)在不要做:
- 在你的應(yīng)用程序的
Capabilities的Background Modes中勾選Background fetch。 - 為
Info.plist添加標(biāo)識符。請為您的刷新任務(wù)。 - 在你的應(yīng)用程序代理中調(diào)用
BGTaskScheduler.register(forTaskWithIdentifier:using:launchHandler:)來處理后臺獲取。 - 創(chuàng)建一個
BGAppRefreshTaskRequest,為何時執(zhí)行指定一個earliestBeginDate。 - 使用
BGTaskScheduler.submit(_:)提交請求。
與后臺完成任務(wù)類似,您有一個很短但不確定的時間框架來執(zhí)行background fetch。共識的數(shù)字是最大30秒,但計劃更少。如果您需要下載大型資源作為獲取的一部分,請使用URLSession的后臺傳輸服務(wù)。
2. Implementing Background Fetch
是時候開始了。首先,簡單的部分:在Signing & Capabilities下選中Background fetch能力。

接下來,打開Info.plist。并點擊+來添加一個新的標(biāo)識符。
向下滾動并選擇Permitted background task scheduler identifiers。展開項目,然后點擊新標(biāo)識符旁邊的+以添加條目。
輸入com.mycompany.myapp.task.refresh獲取標(biāo)識符的值。
注意:在您的實際項目中,您將反向使用您公司的URL作為標(biāo)識符的根,添加您的應(yīng)用程序名稱和描述性元素,如
task.refresh。可以定義多種類型的刷新任務(wù),每種任務(wù)都有自己的標(biāo)識符。

接下來,你需要一個AppDelegate類,因為iOS希望在application(_:didFinishLaunchingWithOptions:)任務(wù)之間注冊你的獲取task。
在App文件夾中,添加一個新的Swift文件AppDelegate.swift。然后將現(xiàn)有代碼替換為:
import UIKit
import BackgroundTasks
class AppDelegate: UIResponder, UIApplicationDelegate {
static var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .long
return formatter
}()
var window: UIWindow?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
return true
}
}
這段代碼為刷新時間戳定義了一個日期格式化器。它還包括一個application(_:didFinishLaunchingWithOptions:)空方法,這是您將注冊后臺獲取任務(wù)的地方。
現(xiàn)在向AppDelegate添加以下函數(shù):
func refresh() {
// to simulate a refresh, just update the last refresh date
// to current date/time
let formattedDate = Self.dateFormatter.string(from: Date())
UserDefaults.standard.set(
formattedDate,
forKey: UserDefaultsKeys.lastRefreshDateKey)
print("refresh occurred")
}
這個函數(shù)模擬了一次刷新。
在您創(chuàng)建的應(yīng)用程序中,您可能會從網(wǎng)絡(luò)獲取數(shù)據(jù)。對于本教程,您將把一個格式化的時間戳保存到UserDefaults中,以顯示刷新執(zhí)行的時間。
仍然在AppDelegate.swift中,向AppDelegate添加以下函數(shù):
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(
identifier: AppConstants.backgroundTaskIdentifier)
request.earliestBeginDate = Date(timeIntervalSinceNow: 1 * 60)
do {
try BGTaskScheduler.shared.submit(request)
print("background refresh scheduled")
} catch {
print("Couldn't schedule app refresh \(error.localizedDescription)")
}
}
這里你創(chuàng)建了一個BGAppRefreshTaskRequest,然后從當(dāng)前時間開始分配一個earliestBeginDate。然后使用BGTaskScheduler.submit(_:)提交請求。
現(xiàn)在,將application(_:didFinishLaunchingWithOptions:)替換為:
BGTaskScheduler.shared.register(
forTaskWithIdentifier: AppConstants.backgroundTaskIdentifier,
using: nil) { task in
self.refresh() // 1
task.setTaskCompleted(success: true) // 2
self.scheduleAppRefresh() // 3
}
scheduleAppRefresh()
return true
當(dāng)iOS完成啟動應(yīng)用程序時,這段代碼向任務(wù)調(diào)度器注冊任務(wù)并調(diào)度第一次刷新。任務(wù)本身,當(dāng)執(zhí)行時,將:

現(xiàn)在需要將AppDelegate連接到AppMain。打開AppMain.swift。在body之前加上這一行:
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
這就是iOS調(diào)用AppDelegate所需要的一切。
在物理設(shè)備上構(gòu)建并運行應(yīng)用程序。檢查Xcode控制臺的消息,以確認(rèn)后臺刷新是預(yù)定調(diào)度的。
3. Testing Background Fetch
測試background fetch的一種方法是坐等系統(tǒng)決定執(zhí)行它。但你可能要坐很長時間等待這一切發(fā)生。
iOS無法保證何時執(zhí)行刷新。該系統(tǒng)使用多種因素來決定何時執(zhí)行,如應(yīng)用程序使用模式、電池充電等。幸運的是,Xcode提供了一種使用調(diào)試器命令觸發(fā)后臺獲取的方法。
打開RefreshView.swift并在print("moved to background")處設(shè)置斷點。
然后將應(yīng)用發(fā)送到后臺,Xcode應(yīng)該在新的斷點處中斷。在lldb提示符下,輸入以下命令(或者,因為它非常復(fù)雜,復(fù)制和粘貼!)
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.mycompany.myapp.task.refresh"]
這將指示調(diào)試器立即執(zhí)行后臺刷新。
恢復(fù)應(yīng)用程序的執(zhí)行??刂婆_應(yīng)該顯示刷新已發(fā)生,然后調(diào)度計劃的后臺刷新。每次刷新都為未來安排另一次刷新。您還可能看到來自后臺任務(wù)調(diào)度器的調(diào)試消息,指示其活動。
接下來,重新打開應(yīng)用程序。 Refresh tab將顯示刷新發(fā)生的時間和日期。
如果你把這款應(yīng)用留在你的設(shè)備上,在接下來的幾天里查看,你會不時看到時間戳的更新。iOS根據(jù)最佳刷新時間的計算調(diào)用刷新。
對于需要很多分鐘才能完成的長時間運行的后臺任務(wù),了解更多關(guān)于 background processing tasks的信息。后臺處理任務(wù)Background processing類似于后臺獲取(background fetch),但用于更嚴(yán)格的任務(wù),如數(shù)據(jù)處理和維護(hù)。
還有兩個與后臺模式(background mode)相關(guān)的很棒的WWDC演講:
-
Advances in App Background Execution : 討論
background processing選項的最新改進(jìn)。 -
Background Execution Demystified : 將幫助你對不同的
background mode有更深的理解。
最后,您可以在Configuring Background Execution Modes 中了解所有的后臺執(zhí)行模式。
后記
本篇主要講述了
Background Modes幾種Mode使用示例,感興趣的給個贊或者關(guān)注~~~
