版本记录
版本号 | 时间 |
---|---|
V1.0 | 2022.11.06 星期日 |
前言
Background Modes
我们在程序中总会用到,包括语音、定位更新、后台任务以及远程通知等,这个模块我们就一起来学习下。
开始
Background Modes
是我们常用的模式,比如语音、定位更新、后台任务以及远程通知等。Xcode里的后台模式如下所示:
在本教程中,您将创建一个使用音频播放、位置更新、关键任务和后台拉取的应用程序,以了解最常见的后台模式。本文来自翻译。
2010
年,随着iOS 4的发布,苹果开始允许应用程序在后台工作,并从那时起不断发展和改进后台模式。iOS
限制使用后台操作来改善用户体验和延长电池寿命。你的应用可以在后台运行特定的情况,包括:播放音频,更新位置和从服务器获取最新的内容。
如果你的任务不属于允许的类别,后台模式可能不适合你。如果你试图使用超出其作用范围的后台模式来操纵系统,你可能会面临App Store
的拒绝。
在本后台模式教程中,你将了解你的应用程序可以在后台做的四件事:
- Play audio - 播放音频:允许应用程序在后台继续播放音频。
- Receive location updates - 接收位置更新:允许应用程序在后台接收位置更改。
- Complete finite-length critical tasks - 完成有限长度的关键任务:允许应用程序在移动到后台后继续完成关键任务。
- Background Fetch - 后台获取:在iOS调度的时间表上执行后台更新。
在深入研究之前,我们先来快速浏览一下iOS的基本后台模式:
- Audio, AirPlay, and Picture in Picture - 音频,AirPlay,和图片中的图片:当应用程序在后台时播放音频和视频。
- Location Updates - 位置更新:在后台时继续接收位置更新。
- Voice over IP - IP语音:通过因特网发送和接收语音。
-
External accessory communication - 外部配件通信:通过
lightning
接口与外部配件通信。 -
Using Bluetooth LE accessories - 使用蓝牙LE配件:在后台与蓝牙
LE
配件通信。 -
Acting as a Bluetooth LE accessory - 充当蓝牙LE配件:允许应用程序为配件提供蓝牙
LE
信息。 - Background fetch - 后台拉取:执行数据刷新。
- Remote notitifications - 远程通知:发送和接收远程通知。
- Background processing - 后台处理:执行较长的关键进程。
您将向示例应用程序添加上述模式中的四种——音频、定位、后台处理和后台获取(audio, location, background processing and background fetches)
。如果你只对其中的一些模式感兴趣,可以随意跳过,只玩你感兴趣的模式。
注意:要获得完整的效果,您应该在真实的设备上进行操作。在模拟器中,当你忘记一个步骤时,应用程序可能会在后台运行。然后当你切换到真正的设备时,它可能根本无法工作。
在您可以在物理设备上运行项目之前,您必须设置您的development team
,如下所示:
构建并运行示例项目来感受一下Sleepless
,这是一个从不休息的应用程序,因为它在后台做事情。有四个tab
—— 每个覆盖一个模式:
您要添加的第一个capability
是background audio
。
Playing Audio
在物理设备上构建并运行Sleepless
。导航到audio tab
,播放音乐,然后通过返回主屏幕把应用程序放在后台。音乐将停止播放。
打开 AudioModel.swift
该应用程序利用AVQueuePlayer
对歌曲进行排队并按顺序播放。模型观察播放器的currentItem
值以提供视图的更新。
1. Giving Credit Where Credit Is Due
最初的项目包括来自incompetech.com的音频文件,这是一个流行的免版税音乐网站。你可以免费使用带有版权的音乐。这三首歌都是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,了解更多关于
active state
和其他的信息。
2. Testing Audio in the Background
为什么当应用程序进入后台时音乐停止了?好吧,缺了一个关键的部分!
大多数后台模式都不能工作,除非你启用特定的功能,表明应用程序想要在后台运行代码。特例是关键任务完成,任何应用程序都可以执行。
当激活时,音频后台模式告诉iOS继续播放音频,即使应用程序在后台。没错,音频后台模式实际上是自动的。你只需要激活它。
返回Xcode,执行以下操作:
接下来,双击Background Modes
以添加此功能。展开Background Modes
功能,然后勾选Audio, AirPlay, and Picture in Picture
以启用background audio
。
在物理设备上构建并运行应用程序。像以前一样启动音乐,然后离开应用程序。这一次音频将继续。就这么简单!
接下来,你将使用Location updates
后台模式继续接收位置更新,即使应用程序是在后台。
Receiving Location Updates
首先,构建并运行应用程序。选择Location tab
并点击Start
。什么也没有发生,因为你错过了一些重要的步骤。你现在要改变了。
1. Enabling Location Updates
打开LocationModel.swift
。这是为LocationView
提供位置数据的代码。您将对init()
做一个简单的更改。替换以下两行:
mgr.requestWhenInUseAuthorization()
mgr.allowsBackgroundLocationUpdates = false
为
mgr.requestAlwaysAuthorization()
mgr.allowsBackgroundLocationUpdates = true
第一行请求位置更新,即使应用程序没有在使用。第二个请求甚至在后台进行更新。
回到Signing & Capabilities
界面,勾选Location updates
框,让iOS知道你的应用程序想在后台接收位置更新。
除了勾选这个框,iOS还要求你在Info.plist
中设置一个键向用户解释为什么你需要后台更新。如果不包含这一点,位置请求将会无声地失败。
打开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
。
现在,构建并运行,切换到Location tab
,点击Start
。
当它第一次加载时,你会看到你写进你的位置隐私原因的消息。
点击Allow while using app
,在外面或大楼周围散步——尽量不要因为抓口袋妖怪而分心。
位置更新应该开始出现。如果没有,将应用再次发送到后台,以触发Always
提示进行位置跟踪。你也可以使用Settings
应用程序,在Privacy ▸ Location Services ▸ Sleepless
设置中启用Sleepless
应用程序始终跟踪。
如果你将应用程序发送到后台,你仍然会看到控制台中发生的位置更新。
一段时间后,你应该会看到如下内容:
2. Testing Location Mode in the Background
如果你退出应用程序,你应该看到应用程序更新了控制台日志中的位置。再次打开它,可以看到地图上所有的大头针,显示你在步行过程中去过的地方。
如果你正在使用模拟器,你也可以使用它来模拟移动!点击Features ▸ Location
菜单:
非常简单,对吧?打开第三个选项卡和第三个后台模式!
Completing Critical Tasks Upon Moving to the Background
下一个后台模式的正式名称是Extending Your App’s Background Execution Time,任务完成说起来容易一点!
从技术上讲,这根本不是后台模式。你不需要在Capabilities
中声明你的应用程序使用它。它是一个API,当你的应用程序在后台时,允许你在有限的时间内运行任意代码,给你更多的时间来完成关键任务,如保存数据。
1. When to Use Task Completion
Completion
后台模式的一个有效用例是完成一些关键任务,例如保存用户的输入或发布一个事务。有很多可能性。
由于代码是任意的,你可以使用这个API
做几乎任何事情:执行冗长的计算,对图像应用过滤器,渲染一个复杂的3D网格 —— 任何!你的想象力是极限,只要你记住你只有一些时间,而不是无限的时间。稍后,您将设置一个在后台运行的冗长计算,因此您可以看到这个API是如何工作的。
iOS
决定了你的应用程序移到后台后的时间。你被授予的时间没有保证,但你总是可以检查UIApplication.shared.backgroundTimeRemaining
。这会告诉你还剩下多少时间。
一般的,基于观察的共识是你大约有30秒。同样,没有保证,API文档甚至没有给出一个估计——所以不要依赖这个数字。你可能有5分钟或5秒钟的时间,所以你的应用程序需要为中断做好准备。当你的时间快到的时候,iOS会给你回调信号。
2. Setting Up a Completion Task
这里有一个每个计算机科学专业的学生都应该熟悉的常见任务:计算 Fibonacci Sequence中的数字。这里的扭转是,你将应用程序移动到后台后计算这些数字。
打开CompleteTaskModel.swift
,看看已经有什么了。按照目前的情况,该视图将按顺序计算斐波那契数列并显示结果。
如果你现在挂起一个实际设备上的应用程序,计算将停止,并在应用程序再次激活时恢复到原来的位置。你的任务是创建一个后台任务,这样计算就可以一直运行,直到iOS说“时间到!”
你首先需要添加以下内容到CompleteTaskModel
:
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
此属性标识要在后台运行的任务请求。
接下来,在resetcalculation()
之前向CompleteTaskModel
添加以下方法:
func registerBackgroundTask() {
backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
print("iOS has signaled time has expired")
self?.endBackgroundTaskIfActive()
}
}
registerBackgroundTask()
告诉iOS,当应用移动到后台时,你需要更多的时间来完成你正在做的事情。返回的值是这个任务的标识符,这样你就可以告诉iOS你什么时候完成了。在这个调用之后,如果你的应用程序移动到后台,它仍然会得到CPU
时间,直到你调用endBackgroundTask(_:)
。
好吧,至少有一些CPU时间。
3. Ending the Completion Task
如果你在后台一段时间后没有调用endBackgroundTask(_:)
, iOS将调用当你调用beginBackgroundTask(expirationHandler:)
时定义的闭包。这使您有机会停止执行代码。
因此,调用endBackgroundTask(_:)
来告诉系统您已经完成是一个好主意。如果你不调用它并在这个块运行后继续执行代码,iOS将终止你的应用程序!
将这个方法添加到registerBackgroundTask()
下面:
func endBackgroundTaskIfActive() {
let isBackgroundTaskActive = backgroundTask != .invalid
if isBackgroundTaskActive {
print("Background task ended.")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
}
这将结束后台任务,如果它是主动注册的,并将其ID重置为invalid
。
4. Registering and Ending Background Tasks
现在,对于重要的部分:更新onChangeOfScenePhase(_:)
来注册和结束后台任务,这取决于应用程序是移动到后台还是活动状态。
用以下语句替换这两个case语句:
case .background:
let isTimerRunning = updateTimer != nil
let isTaskUnregistered = backgroundTask == .invalid
if isTimerRunning && isTaskUnregistered {
registerBackgroundTask()
}
case .active:
endBackgroundTaskIfActive()
当切换到后台background
状态时,这将在任务正在运行但未注册时注册它。当切换到活动active
状态时,它将结束后台任务。
在beginPauseTask()
中,在updateTimer = nil
之后添加这一行:
endBackgroundTaskIfActive()
现在,当用户停止计算时,你调用endBackgroundTask(_:)
来告诉iOS你不需要任何额外的CPU时间。
注意:每次调用
beginBackgroundTask(expirationHandler:)
时调用endBackgroundTask(_:)
是很重要的。如果你调用beginBackgroundTask(expirationHandler:)
两次,并且只对其中一个任务调用endBackgroundTask(_:)
,你仍然会得到CPU
时间,直到你使用第二个后台任务的标识符第二次调用endBackgroundTask(_:)
。
构建并运行,然后切换到第三个选项卡。
点击Play
并观看应用程序计算这些甜蜜的斐波那契值。将应用发送到后台,但要观察Xcode控制台的输出。当剩下的时间减少时,你的应用程序应该继续更新数字。
在大多数情况下,这个时间从30秒开始,一直到5秒。如果你在达到5秒时等待时间过期——或者你看到的任何值——iOS就会调用过期block
。
你的应用程序应该很快停止产生输出。然后,如果你回到应用程序,计时器应该会再次启动,斐波那契疯狂将继续。
在前台和后台之间切换,看看如何通过每次切换获得额外的时间块。
下面是本教程的最后一个主题:background fetch
Background Fetch
Background fetch
是在iOS 7
中引入的。它可以让你的应用程序显示最新的同时最小化对电池寿命的影响。从iOS 13
开始,苹果引入了一个新的后台任务调度程序API,提供了显著的改进。
例如,假设你正在应用程序中实现一个新闻feed
。在后台获取之前,你将在应用程序每次启动时刷新feed
。
遗憾的是,当刷新时,用户会看到几秒钟的旧标题。你知道,有些人会试图挖掘一个故事,结果却发现它消失了,取而代之的是一个不相关的故事。看起来不太好。
如果当用户打开你的应用时,最新的标题就会神奇地出现在那里,不是更好吗?这是后台获取给你的能力。
当启用时,系统利用使用模式来确定何时触发后台获取。例如,如果用户在上午9点打开你的新闻应用,background fetch
可能会在上午9点之前发生。系统决定发出background fetch
的最佳时间,由于这个原因,它不适合进行关键更新。
1. Understanding Background Fetch
Background fetch
由BGTaskScheduler
控制,这是一个复杂的系统,用于平衡所有影响用户体验的因素,如性能、使用模式、电池寿命等。
Background fetch
通常涉及从外部来源(如网络服务)获取信息。在本后台模式教程中,您将获取当前时间,而不使用网络。
为了实现background fetch
,你需要完成这些任务——但现在不要做:
- 在你的应用程序的
Capabilities
的Background Modes
中勾选Background fetch
。 - 为
Info.plist
添加标识符。请为您的刷新任务。 - 在你的应用程序代理中调用
BGTaskScheduler.register(forTaskWithIdentifier:using:launchHandler:)
来处理后台获取。 - 创建一个
BGAppRefreshTaskRequest
,为何时执行指定一个earliestBeginDate
。 - 使用
BGTaskScheduler.submit(_:)
提交请求。
与后台完成任务类似,您有一个很短但不确定的时间框架来执行background fetch
。共识的数字是最大30
秒,但计划更少。如果您需要下载大型资源作为获取的一部分,请使用URLSession
的后台传输服务。
2. Implementing Background Fetch
是时候开始了。首先,简单的部分:在Signing & Capabilities
下选中Background fetch
能力。
接下来,打开Info.plist
。并点击+
来添加一个新的标识符。
向下滚动并选择Permitted background task scheduler identifiers
。展开项目,然后点击新标识符旁边的+
以添加条目。
输入com.mycompany.myapp.task.refresh
获取标识符的值。
注意:在您的实际项目中,您将反向使用您公司的URL作为标识符的根,添加您的应用程序名称和描述性元素,如
task.refresh
。可以定义多种类型的刷新任务,每种任务都有自己的标识符。
接下来,你需要一个AppDelegate
类,因为iOS希望在application(_:didFinishLaunchingWithOptions:)
任务之间注册你的获取task
。
在App
文件夹中,添加一个新的Swift
文件AppDelegate.swift
。然后将现有代码替换为:
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:)
空方法,这是您将注册后台获取任务的地方。
现在向AppDelegate
添加以下函数:
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")
}
这个函数模拟了一次刷新。
在您创建的应用程序中,您可能会从网络获取数据。对于本教程,您将把一个格式化的时间戳保存到UserDefaults
中,以显示刷新执行的时间。
仍然在AppDelegate.swift
中,向AppDelegate
添加以下函数:
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)")
}
}
这里你创建了一个BGAppRefreshTaskRequest
,然后从当前时间开始分配一个earliestBeginDate
。然后使用BGTaskScheduler.submit(_:)
提交请求。
现在,将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
当iOS完成启动应用程序时,这段代码向任务调度器注册任务并调度第一次刷新。任务本身,当执行时,将:
现在需要将AppDelegate
连接到AppMain
。打开AppMain.swift
。在body
之前加上这一行:
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
这就是iOS
调用AppDelegate
所需要的一切。
在物理设备上构建并运行应用程序。检查Xcode
控制台的消息,以确认后台刷新是预定调度的。
3. Testing Background Fetch
测试background fetch
的一种方法是坐等系统决定执行它。但你可能要坐很长时间等待这一切发生。
iOS
无法保证何时执行刷新。该系统使用多种因素来决定何时执行,如应用程序使用模式、电池充电等。幸运的是,Xcode
提供了一种使用调试器命令触发后台获取的方法。
打开RefreshView.swift
并在print("moved to background")
处设置断点。
然后将应用发送到后台,Xcode应该在新的断点处中断。在lldb
提示符下,输入以下命令(或者,因为它非常复杂,复制和粘贴!)
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.mycompany.myapp.task.refresh"]
这将指示调试器立即执行后台刷新。
恢复应用程序的执行。控制台应该显示刷新已发生,然后调度计划的后台刷新。每次刷新都为未来安排另一次刷新。您还可能看到来自后台任务调度器的调试消息,指示其活动。
接下来,重新打开应用程序。 Refresh tab
将显示刷新发生的时间和日期。
如果你把这款应用留在你的设备上,在接下来的几天里查看,你会不时看到时间戳的更新。iOS根据最佳刷新时间的计算调用刷新。
对于需要很多分钟才能完成的长时间运行的后台任务,了解更多关于 background processing tasks的信息。后台处理任务Background processing
类似于后台获取(background fetch)
,但用于更严格的任务,如数据处理和维护。
还有两个与后台模式(background mode)
相关的很棒的WWDC
演讲:
-
Advances in App Background Execution : 讨论
background processing
选项的最新改进。 -
Background Execution Demystified : 将帮助你对不同的
background mode
有更深的理解。
最后,您可以在Configuring Background Execution Modes 中了解所有的后台执行模式。
后记
本篇主要讲述了
Background Modes
几种Mode使用示例,感兴趣的给个赞或者关注~~~