本文主要内容来自WWDC 2019: Advances in App Background Execution

Apple 中很多后台履行都是用户从前台进入后台之后,仍然坚持了一段时刻的活泼,最常见的比方运用UIApplication.shared.beginBackgroundTask() 来恳求更长的代码履行时刻,一起不同的线程后台恳求履行使命的时刻也不同,这一点在我的另一篇文章中亦有深入的探索。

在iOS 13.0之后,Apple出了新的结构 BackgroundTasks ,这个和前者是有很大的不同的,那便是它并不会从前台到后台之后立马履行,而是会规划后台使命履行的时刻,体系主动挑选适宜的时刻来履行该使命,比方手机充电或许搁置的时分

它总共供给了两个Task来履行,分别是 BGProcessingTaskBGAppRefreshTask

纵观iOS的后台使命的机制,基本能够分为两类,一类是当即履行的后台使命,比方从前台到后台恳求后台履行时刻完结前台使命、收到后台推送处理内容等等,这一类是当即履行后台使命的类型,还有一类便是延时履行的后台使命,由体系挑选适宜的时刻来履行使命。

当即履行的后台使命

App怎么进入当即履行后台使命的状态呢?也便是当即进入 Background 状态,一般是两种办法:

  • App恳求:App想完结某些使命比方下载等等,所以向体系恳求后台履行时刻
  • 事件触发:App需求履行后台使命来呼应某些事件,比方音讯推送等等

下面以Message App为例,它涉及到诸多场景都是这种当即履行后台使命的状况。

Send Messages

当服务器呼应很慢的时分,用户或许发送了音讯之后就将手机锁屏了,这种状况需求去保证音讯在后台状态下也能够成功发送。 这种在后台完结前台的使命还有一些场景,比方保存文件到磁盘中、完结用户恳求等等。

这种在前台进入后台后需求额定的时刻来履行使命的场景需求运用 beginBackgroundTask(expirationHandler:) 办法,假如app是在Extension中运转的话,那就需求运用 ProcessInfo.performExpiringActivity(withReason:using:) 办法,代码实例如下:

func send(_ message: Message) {
    let sendOperation = SendOperation(message: message)
    var identifier: UIBackgroundTaskIdentifier!
    identifier = UIApplication.shared.beginBackgroundTask(expirationHandler: {
        sendOperation.cancel()
        postUserNotification("Message not sent, please resend")
     // Background task will be ended in the operation's completion block below
     })
    sendOperation.completionBlock = {
        UIApplication.shared.endBackgroundTask(identifier)
    }
    operationQueue.addOperation(sendOperation)
}

留意 beginBackgroundTaskendBackgroundTask 需求成对运用。也有或许在体系分配届时刻内仍然无法完结改使命,那么这个时分就会履行 expirationHandler ,在这里将做失利处理,在样例代码中发送了一条本地告诉,提示用户音讯并未成功发送!

Phone Calls

当有人给你打电话的时分需求向用户呈现来电提示,这个场景运用了 VoIP push notifications

这个API,这是一种特别的推送能够发动App,来让用户接听电话,需求在PK推送注册中注册VoIP类型:

func registerForVoIPPushs() {
	self.voipRegistry = PKPushRegistry(queue: nil)
	self.voipRegistry.delegate = self
	self.voipRegistry.desiredPushTypes = [.voIP]
}

可是在2019年有一个新的改进,那便是在 didReceiveIncomingPush 回调中有必要运用 CallKit 结构来陈述来电,**否则体系将停止杀死App。**假如一向无法处理该告诉,那么体系或许在接收到 VoIP 推送之后再也不会发动App了。那么新的改变如下:

let provider = CXProvider(configuration: providerConfiguration)
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
    if type == .voIP {
        if let handle = payload.dictionaryPayload["handle"] as? String {
            let callUpdate = CXCallUpdate()
            callUpdate.remoteHandle = CXHandle(type: .phoneNumber,
                                               value: handle)
            let callUUID = UUID()
            provider.reportNewIncomingCall(with: callUUID,
                                           update: callUpdate) { _ in
                completion()
            }
            establishConnection(for: callUUID)
        }
    }
}

有几点需求留意的细节:

  • 推送中带有满足的来电人的信息,能够用来展示UI界面。
  • apns-expiration 推送设置为0或许很小的值,这样来电之后的告诉也会是相关通话的告诉,而不是几分钟甚至更久之后,来电结束了才收到告诉。
  • 运用标准推送(standard push),不用全屏推送,无需在呼叫UI中全屏展示告诉。
  • 还能够运用 Notification Service Extension 来修正内容。

Muted Threads (静音群组)

像微信一样,有多个联系人,以及群聊的时分,有时用户不想让某些群聊的音讯有提示,可是进入App中之后又想要当即查看信息,仅仅不想每次都轰动设备并收到告诉。为了到达这一点,我们需求 Background Pushes 后台推送机制。这个机制能够告诉设备有新数据可用而无需提示用户。

这就需求设置推送 content-available: 1, 而不是 alert, sound, 或许 badge ,从而完成静默推送的意图,体系收到告诉之后会挑选一个适宜的时刻来发动app来下载相关内容,时刻线如下:

了解iOS的后台任务执行的各种方式

一起后台推送功用增加了一些新的机制:

  • apns-priority = 5 ,有必要优先级设置为5,否则体系无法后台发动app。
  • 比方设置 apns-push-type = background ,这对 watchOS 是有必要的,可是Apple主张一切渠道关于后台静默推送都采用这种办法。

Download Past Attachments(下载之前的附件)

假如用户在一台新的设备上登录了它的账户,需求当即下载回话列表以及最近的音讯记录,可是对玉一些很老的内容,假如能够在设备充电或许搁置时下载的话,何必在前台下载呢?所以这就需求推延后台履行下载的时刻,完成办法是 Discretionary Background URL Session

let config = URLSessionConfiguration.background(withIdentifier: "com.app.attachments")
let session = URLSession(configuration: config, delegate: ..., delegateQueue: ...)
// 设置体系自主性:根据功能来决议开始时刻
config.discretionary = true
// 设置时刻距离
config.timeoutIntervalForResource = 24 * 60 * 60
config.timeoutIntervalForRequest = 60
// 创立恳求
var request = URLRequest(url: url)
request.addValue("...", forHTTPHeaderField: "...")
let task = session.downloadTask(with: request)
// 设置恳求组织的最早时刻,这里是两小时后
task.earliestBeginDate = Date(timeIntervalSinceNow: 2 * 60 * 60)
// 设置估计的作业量
task.countOfBytesClientExpectsToSend = 160
task.countOfBytesClientExpectsToReceive = 4096
task.resume()

延时的后台使命

以上都是一些当即履行的后台使命,接下来要介绍的不是马上就履行的后台使命,而是延迟履行的后台使命,会在设备搁置或许充电的时分统一来进行使命的处理:

了解iOS的后台任务执行的各种方式

Background Processing Task的特色

留意,这里是的数据来自Apple的WWDC视频,所以应该要相信它的准确性。

  • 体系会在适宜的时分分配几分钟的运转时刻

    • 履行可推延的可维护性作业:同步数据、备份、本地数据库清理等等
    • Core ML的训练等等
  • 关于核算密集型的操作能够关掉 CPU 的监控使后台使命充分利用硬件功能

    其实这便是为了后台进行模型训练来特意整出来的!!!

  • 在前台恳求过,那么在后台就能够履行

在运用 BGProcessingTaskRequest 时有几个属性需求留意:

  • requiresNetworkConnectivity

    假如在履行后台使命的时分需求运用网络,而不仅仅是本地的操作,那属性就要设置为 true 。

  • requiresExternalPower

    后台使命履行核算密集型的操作的时分,想要撤销 CPU 的监控,能够设置改属性为 true 来完成这一点。

Background App Refresh Task的特色

新的API,后台改写使命。

  • 该使命供给30秒的运转时刻

  • 用于获取新内容使App坚持最新的数据状态

  • 后台改写使命履行的机遇取决于用户运用App的办法

    假如用户在早中晚运用App,那么它能够在运用之前发动该App的后台使命来获取最新数据。

了解iOS的后台任务执行的各种方式

运用频率不高的状况:会在发动之前,调用后台改写使命

还有一点要留意的是运用新的API之后,不要运用旧的API了,旧的API现已被废弃了:

UIApplication.setMinimumBackgroundFetchInterval(_:)
UIApplicationDelegate.application(_:performFetchWithCompletionHandler:)

运用BackgroundTasks的原理

了解iOS的后台任务执行的各种方式

App以及它的Extension都能够创立 BGTask ,并将其提交给 BGTaskScheduler ,它是一个大局的管理后台使命的进程,它会在适宜的时分挑选履行相应的Task,唤醒App并在后台发动它履行对应的使命,完结使命之后,需求调用 setTaskCompleted 办法,将使命标记为完结并挂起App。

一起Extension提交的使命只会唤醒主App,也便是说 BackgroundTask 永久由主App来履行。 体系也或许挑选后台发动App来一起履行多个使命,可是体系只会按照每次发动来分配一定的时刻来一起履行使命,并不会按照使命来独自分配时刻。

运用Background Task的流程

这个在Apple的文档中讲述的非常得清楚:****Using background tasks to update your app。**主要是有几个重点的步骤:

  1. 在项意图 capabilities 中开启想要的后台使命: BGAppRefreshTask 以及 BGProcessingTask

了解iOS的后台任务执行的各种方式

  1. 在Target的Info中添加[BGTaskSchedulerPermittedIdentifiers](https://developer.apple.com/documentation/bundleresources/information_property_list/bgtaskschedulerpermittedidentifiers) 中相应的identifier字符串来标识task,后续需求在代码中注册

了解iOS的后台任务执行的各种方式

  1. 运用设置好的Identifier注册BGTaskScheduler
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let feedVC = (window?.rootViewController as? UINavigationController)?.viewControllers.first as? FeedTableViewController
    feedVC?.server = server
    PersistentContainer.shared.loadInitialData()
    // MARK: Registering Launch Handlers for Tasks
    BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.apple-samplecode.ColorFeed.refresh", using: nil) { task in
        // Downcast the parameter to an app refresh task as this identifier is used for a refresh request.
        self.handleAppRefresh(task: task as! BGAppRefreshTask)
    }
    BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.apple-samplecode.ColorFeed.db_cleaning", using: nil) { task in
        // Downcast the parameter to a processing task as this identifier is used for a processing request.
        self.handleDatabaseCleaning(task: task as! BGProcessingTask)
    }
    return true
}
  1. 在适宜的机遇提交相应的Request
func applicationDidEnterBackground(_ application: UIApplication) {
    scheduleAppRefresh()
}
// MARK: - Scheduling Tasks
func scheduleAppRefresh() {
    let request = BGAppRefreshTaskRequest(identifier: "com.example.apple-samplecode.ColorFeed.refresh")
    request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // Fetch no earlier than 15 minutes from now
    do {
        try BGTaskScheduler.shared.submit(request)
    } catch {
        print("Could not schedule app refresh: \(error)")
    }
}

另外还有一些额定需求留意的点:

  • 不要设置 earliestBeginDate 太远,最好在一周之内

  • 保证在设备被锁住的时分,仍然能够访问文件

    FileProectionType.completeUntilFirstUserAuthentication
    
  • UIScene apps较为特别,需求用到UIApplication.requestSceneSessionRefresh(_:) API

  • BGTaskScheduler.submit 为了运用的简练是设置为一个阻塞的同步调用,所以假如要在发动的时分提交,那么应该在 background queue 中运用, 而非 main queue。

总结:后台使命怎么挑选?

既然上面现已描述了这么多的后台使命,那么究竟该怎么挑选呢?以及是几种常见的场景下的挑选,具体Case来自Apple的官方文档:Choosing Background Strategies for Your App。

1、在后台继续前台使命

运用[beginBackgroundTask(withName:expirationHandler:)] 恳求时刻继续履行前台的使命。

2、延迟履行核算密集型作业

运用 [BGProcessingTask] ,由体系来决议最佳的使命履行时刻点。

3、更新App中的内容

假如App是周期性的从服务器拉取数据,那就能够运用 [BGAppRefreshTask] ,由体系挑选最佳的使命履行时刻点,并且这种办法能够供给最多30秒的后台履行时刻。

4、运用后台推送唤醒App

运用后台推送在后台静默唤醒App,不涉及 alert、sound 以及 badge。这个上述现已介绍过了,就不赘述了。

5、运用后台推送告诉用户

假如app需求在后台履行使命,并且还要向用户展示告诉,那么能够运用 Notification Serverce Extension 。在收到推送告诉之后,这个 service extension 会被唤醒,并且通过 [didReceive(_:withContentHandler:)] 来恳求后台履行时刻。

当 extension 完结使命之后,它有必要调用 content handler 闭包来处理给用户的内容。extension 的履行时刻也是有限的。

问题:App想长时刻在后台运转怎么办?

在Apple官方文档中Preparing your UI to run in the background中总结了App在进入后台之后还能够履行使命的几种状况:

  • Audio communication using AirPlay, or Picture in Picture video.
  • Location-sensitive services for users.
  • Voice over IP.
  • Communication with an external accessory.
  • Communication with Bluetooth LE accessories, or conversion of the device into a Bluetooth LE accessory.
  • Regular updates from a server.
  • Support for Apple Push Notification service (APNs).

而假如想一向在后台运转,那就需求继续的在后台履行使命,占据体系资源,一般来说有以下三种状况:

  • 播映音频或许视频
  • 后台继续定位
  • 连接Bluetooth LE accessories

要留意的是,这三种状况都需求在开发的时分设置 Background Modes。

引证

[1] Apple 文档 Preparing your UI to run in the background

[2] Apple 文档 Choosing Background Strategies for Your App

[3] Apple 文档 Using background tasks to update your app

[4] WWDC 2019: Advances in App Background Execution