Background Modes详细解析(一) —— 几种Mode使用示例(一)

版本记录

版本号 时间
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 —— 每个覆盖一个模式:

您要添加的第一个capabilitybackground 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 DescriptionPrivacy — 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 fetchBGTaskScheduler控制,这是一个复杂的系统,用于平衡所有影响用户体验的因素,如性能、使用模式、电池寿命等。

Background fetch通常涉及从外部来源(如网络服务)获取信息。在本后台模式教程中,您将获取当前时间,而不使用网络。

为了实现background fetch,你需要完成这些任务——但现在不要做:

  • 在你的应用程序的CapabilitiesBackground 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演讲:

最后,您可以在Configuring Background Execution Modes 中了解所有的后台执行模式。

后记

本篇主要讲述了Background Modes几种Mode使用示例,感兴趣的给个赞或者关注~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容