在日常使用情况下,手机上通常同时运行着多个应用,某个应用在前台时,其它的应用在后台。在资源有限的情况下,为了保证设备尽可能的流畅,系统会为每个应用智能分配 CPU 及内存的阈值,一旦应用超过对应阈值,将会被系统终止。
我们日常开发中发生的 OOM(Out Of Memory) 以及主线程长时间未响应而触发系统的“看门狗”,都是由于应用耗尽了系统分配的资源而被系统终止。
延展阅读
触发“看门狗”通常会生成一份 Crash 日志,日志内容类似下面这样,经典的 0x8badf00d
Exception Type: EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Termination Reason: Namespace SPRINGBOARD, Code 0x8badf00d
Termination Description: SPRINGBOARD, process-launch watchdog transgression: com.xxxx exhausted real (wall clock) time allowance of 20.00 seconds | | ProcessVisibility: Unknown | ProcessState: Running | WatchdogEvent: process-launch | WatchdogVisibility: Foreground | WatchdogCPUStatistics: ( | “Elapsed total CPU time (seconds): 2.910 (user 2.910, system 0.000), 7% CPU”, | “Elapsed application CPU time (seconds): 0.000, 0% CPU” | )
Triggered by Thread: 0
如果对系统 Crash 日志感兴趣,可以看看我去年写的这篇文章 WWDC 2018:理解崩溃以及崩溃日志
由于应用在执行后台任务时,用户是无感的,但是用户对于自己的隐私信息是敏感的,所以在相关 API 的设计时会告知用户,哪些数据会被使用。
从今年的 WWDC 的动作来看,苹果对用户的隐私越来越重视,这点非常值得称赞,比如今年推出的 Sign In With Apple、地理位置权限的变更、后台地理位置访问的弹窗等。当然,这不是开始也不是结束,为苹果爸爸点赞👍。
// Guarding Important Tasks While App is Still in the Foregroundfunc send(_ message: Message) { let sendOperation = SendOperation(message: message) var identifier: UIBackgroundTaskIdentifier! // 1 identifier = UIApplication.shared.beginBackgroundTask(expirationHandler: { // 2 sendOperation.cancel() postUserNotification("Message not sent, please resend") // Background task will be ended in the operation's completion block below }) sendOperation.completionBlock = { // 3 UIApplication.shared.endBackgroundTask(identifier) } operationQueue.addOperation(sendOperation)}
让我们依次看看上面标注的步骤:
应用在前台时通过对应 API 创建一个后台任务,此时即使 app 进入后台,也会获得一定的时间来处理消息发送。
更多信息可以查看Downloading Files in the Background。这里值得注意的是,如果在后台任务下载过程中应用被系统终止,再次启动时,使用相同 identifier 创建的 session 系统将会从上一次终止的地方继续下载对应内容。但是如果用户手动通过多任务将应用终止的话,系统会取消所有后台下载任务,同时系统也不会自动在后台唤起应用。
// task@available(iOS 13.0, *)open class BGTask : NSObject { open var identifier: String { get } open var expirationHandler: (() -> Void)? open func setTaskCompleted(success: Bool)}@available(iOS 13.0, *)open class BGProcessingTask : BGTask {}@available(iOS 13.0, *)open class BGAppRefreshTask : BGTask {}// request@available(iOS 13.0, *)open class BGTaskRequest : NSObject, NSCopying { open var identifier: String { get } open var earliestBeginDate: Date?}@available(iOS 13.0, *)open class BGAppRefreshTaskRequest : BGTaskRequest { public init(identifier: String)}@available(iOS 13.0, *)open class BGProcessingTaskRequest : BGTaskRequest { public init(identifier: String) open var requiresNetworkConnectivity: Bool open var requiresExternalPower: Bool}// scheduler@available(iOS 13.0, *)open class BGTaskScheduler : NSObject { open class var shared: BGTaskScheduler { get } open func register(forTaskWithIdentifier identifier: String, using queue: DispatchQueue?, launchHandler: @escaping (BGTask) -> Void) -> Bool open func submit(_ taskRequest: BGTaskRequest) throws open func cancel(taskRequestWithIdentifier identifier: String) open func cancelAllTaskRequests() open func getPendingTaskRequests(completionHandler: @escaping ([BGTaskRequest]) -> Void)}
这种后台模式会给应用几分钟的时间来处理相关任务,相比之前的几十秒有了比较大的提升。因此我们可以将一些可延迟到后台执行的任务放到这种模式下执行,也可以将一些 Core ML 的训练放到这种模式下执行。
最重要的一点是,新框架允许我们关掉 CPU 的检测,因为之前系统出于对电池寿命的考虑,会将后台 CPU 占用较高的应用“杀死”,所以新框架的这个特性对于那些 CPU 占用较高的后台任务可以说是及时雨了,而要做到这个,仅仅只需要设置 bgProcessingTaskRequest.requiresExternalPower = true 即可。
会根据用户使用应用的频次和时间段,来决定何时触发后台刷新任务。比如用户经常在早上 8 点和晚上 10 点会打开应用,系统则会在这两个时间点之前触发刷新任务,以保证用户总是看到最新的内容。这也就意味着如果应用使用的频次较低,系统触发的刷新任务的频次也就随之变低。同时下面两个 API 被废弃了,虽然在iOS、iPadOS、tvOS 任能使用,但是在 Mac 上将无法使用,所以尽快切到新的 API 吧~
- (void)setMinimumBackgroundFetchInterval:(NSTimeInterval)minimumBackgroundFetchInterval API_DEPRECATED("Use a BGAppRefreshTask in the BackgroundTasks framework instead", ios(7.0, 13.0), tvos(11.0, 13.0));- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler API_DEPRECATED("Use a BGAppRefreshTask in the BackgroundTasks framework instead", ios(7.0, 13.0), tvos(11.0, 13.0));