## 变更内容总结 1. **iOS后台任务系统重构** - 修复后台任务无法自动运行的问题 2. **日志系统优化** - 改进日志记录机制,添加队列和批量写入 3. **文档新增** - 添加后台任务修复总结和测试指南文档 4. **应用启动优化** - 添加后台任务状态检查和恢复逻辑 5. **版本号更新** - Info.plist版本从1.0.23升级到1.0.24 ## 提交信息类型判断 - **主要类型**: `fix` - 这是一个重要的bug修复,解决了iOS后台任务无法自动运行的核心问题 - **作用域**: `ios-background` - 专注于iOS后台任务功能 - **影响**: 这个修复对iOS用户的后台功能至关重要 ## 提交信息 fix(ios-background): 修复iOS后台任务无法自动运行的问题 主要修复内容: - 修复BackgroundTaskBridge任务调度逻辑,改用BGAppRefreshTaskRequest - 添加任务完成后自动重新调度机制,确保任务持续执行 - 优化应用生命周期管理,移除重复的后台任务调度 - 在应用启动时添加后台任务状态检查和恢复功能 - 将默认任务间隔从30分钟优化为15分钟 次要改进: - 重构日志系统,添加内存队列和批量写入机制,提升性能 - 添加写入锁和重试机制,防止日志数据丢失 - 新增详细的修复总结文档和测试指南 技术细节: - 使用BGAppRefreshTaskRequest替代BGProcessingTaskRequest - 实现任务过期自动重新调度 - 添加任务执行状态监控和恢复逻辑 - 优化错误处理和日志输出 影响范围: iOS后台任务调度、通知推送、应用状态管理
468 lines
13 KiB
Swift
468 lines
13 KiB
Swift
//
|
||
// BackgroundTaskBridge.swift
|
||
// OutLive
|
||
//
|
||
// Native bridge responsible for scheduling and relaying iOS background tasks to the JS runtime.
|
||
//
|
||
|
||
import BackgroundTasks
|
||
import Foundation
|
||
import React
|
||
import UIKit
|
||
|
||
@objc(BackgroundTaskBridge)
|
||
class BackgroundTaskBridge: RCTEventEmitter {
|
||
|
||
private enum TaskKind: String {
|
||
case processing
|
||
case refresh
|
||
}
|
||
|
||
private let queue = DispatchQueue(label: "com.anonymous.digitalpilates.background.bridge", qos: .utility)
|
||
|
||
private var hasListeners = false
|
||
private var identifier: String?
|
||
private var kind: TaskKind = .processing
|
||
private var requiresNetworkConnectivity = false
|
||
private var requiresExternalPower = false
|
||
private var defaultDelay: TimeInterval = 60 * 30 // 30 minutes
|
||
private var currentTask: BGTask?
|
||
|
||
override init() {
|
||
super.init()
|
||
// 监听来自 AppDelegate 的后台任务通知
|
||
NotificationCenter.default.addObserver(
|
||
self,
|
||
selector: #selector(handleTaskNotification(_:)),
|
||
name: NSNotification.Name("BackgroundTaskBridge.handleTask"),
|
||
object: nil
|
||
)
|
||
}
|
||
|
||
deinit {
|
||
NotificationCenter.default.removeObserver(self)
|
||
}
|
||
|
||
override static func requiresMainQueueSetup() -> Bool {
|
||
true
|
||
}
|
||
|
||
override func startObserving() {
|
||
hasListeners = true
|
||
}
|
||
|
||
override func stopObserving() {
|
||
hasListeners = false
|
||
}
|
||
|
||
@objc
|
||
private func handleTaskNotification(_ notification: Notification) {
|
||
guard #available(iOS 13.0, *),
|
||
let userInfo = notification.userInfo,
|
||
let task = userInfo["task"] as? BGTask else {
|
||
return
|
||
}
|
||
|
||
NSLog("[BackgroundTaskBridge] 收到来自 AppDelegate 的后台任务")
|
||
handle(task: task)
|
||
}
|
||
|
||
override func supportedEvents() -> [String]! {
|
||
[
|
||
"BackgroundTaskBridge.execute",
|
||
"BackgroundTaskBridge.expire"
|
||
]
|
||
}
|
||
|
||
@objc
|
||
func configure(
|
||
_ options: NSDictionary,
|
||
resolver: @escaping RCTPromiseResolveBlock,
|
||
rejecter: @escaping RCTPromiseRejectBlock
|
||
) {
|
||
guard #available(iOS 13.0, *) else {
|
||
rejecter(
|
||
"BACKGROUND_TASK_UNAVAILABLE",
|
||
"BGTaskScheduler requires iOS 13 or later.",
|
||
nil
|
||
)
|
||
return
|
||
}
|
||
|
||
guard let identifier = options["identifier"] as? String,
|
||
!identifier.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||
rejecter("INVALID_IDENTIFIER", "A non-empty identifier is required.", nil)
|
||
return
|
||
}
|
||
|
||
let taskTypeRaw = (options["taskType"] as? String)?.lowercased()
|
||
let taskKind = TaskKind(rawValue: taskTypeRaw ?? "") ?? .processing
|
||
|
||
let requiresNetwork = (options["requiresNetworkConnectivity"] as? Bool) ?? false
|
||
let requiresPower = (options["requiresExternalPower"] as? Bool) ?? false
|
||
let defaultDelaySeconds = (options["defaultDelay"] as? NSNumber)?.doubleValue
|
||
|
||
queue.async { [weak self] in
|
||
guard let self else { return }
|
||
|
||
self.identifier = identifier
|
||
self.kind = taskKind
|
||
self.requiresNetworkConnectivity = requiresNetwork
|
||
self.requiresExternalPower = requiresPower
|
||
|
||
if let delay = defaultDelaySeconds, delay > 0 {
|
||
self.defaultDelay = delay
|
||
}
|
||
|
||
do {
|
||
try self.scheduleTask(after: self.defaultDelay)
|
||
resolver([
|
||
"identifier": identifier,
|
||
"scheduled": true,
|
||
"defaultDelay": self.defaultDelay
|
||
])
|
||
} catch {
|
||
rejecter("BACKGROUND_TASK_ERROR", error.localizedDescription, error)
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc
|
||
func schedule(
|
||
_ options: NSDictionary,
|
||
resolver: @escaping RCTPromiseResolveBlock,
|
||
rejecter: @escaping RCTPromiseRejectBlock
|
||
) {
|
||
guard #available(iOS 13.0, *) else {
|
||
rejecter(
|
||
"BACKGROUND_TASK_UNAVAILABLE",
|
||
"BGTaskScheduler requires iOS 13 or later.",
|
||
nil
|
||
)
|
||
return
|
||
}
|
||
|
||
guard let identifier = identifier else {
|
||
rejecter("NOT_CONFIGURED", "Call configure() before scheduling.", nil)
|
||
return
|
||
}
|
||
|
||
let delay = (options["delay"] as? NSNumber)?.doubleValue
|
||
let effectiveDelay = (delay?.isFinite == true && delay! >= 0) ? delay! : defaultDelay
|
||
|
||
queue.async { [weak self] in
|
||
guard let self else { return }
|
||
|
||
do {
|
||
try self.scheduleTask(after: effectiveDelay)
|
||
resolver([
|
||
"identifier": identifier,
|
||
"scheduled": true,
|
||
"delay": effectiveDelay
|
||
])
|
||
} catch {
|
||
rejecter("BACKGROUND_TASK_ERROR", error.localizedDescription, error)
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc
|
||
func complete(
|
||
_ success: NSNumber,
|
||
rescheduleAfter: NSNumber?,
|
||
resolver: @escaping RCTPromiseResolveBlock,
|
||
rejecter: @escaping RCTPromiseRejectBlock
|
||
) {
|
||
guard #available(iOS 13.0, *) else {
|
||
rejecter(
|
||
"BACKGROUND_TASK_UNAVAILABLE",
|
||
"BGTaskScheduler requires iOS 13 or later.",
|
||
nil
|
||
)
|
||
return
|
||
}
|
||
|
||
let shouldRescheduleAfter = rescheduleAfter?.doubleValue ?? defaultDelay
|
||
let completionSuccess = success.boolValue
|
||
|
||
queue.async { [weak self] in
|
||
guard let self else { return }
|
||
|
||
if let task = self.currentTask {
|
||
task.setTaskCompleted(success: completionSuccess)
|
||
self.currentTask = nil
|
||
}
|
||
|
||
do {
|
||
try self.scheduleTask(after: shouldRescheduleAfter)
|
||
resolver([
|
||
"rescheduled": true,
|
||
"delay": shouldRescheduleAfter,
|
||
"success": completionSuccess
|
||
])
|
||
} catch {
|
||
rejecter("BACKGROUND_TASK_ERROR", error.localizedDescription, error)
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc
|
||
func cancelAll(
|
||
_ resolver: @escaping RCTPromiseResolveBlock,
|
||
rejecter: @escaping RCTPromiseRejectBlock
|
||
) {
|
||
guard #available(iOS 13.0, *) else {
|
||
rejecter(
|
||
"BACKGROUND_TASK_UNAVAILABLE",
|
||
"BGTaskScheduler requires iOS 13 or later.",
|
||
nil
|
||
)
|
||
return
|
||
}
|
||
|
||
queue.async {
|
||
BGTaskScheduler.shared.cancelAllTaskRequests()
|
||
resolver(["cancelled": true])
|
||
}
|
||
}
|
||
|
||
@objc
|
||
func getPendingRequests(
|
||
_ resolver: @escaping RCTPromiseResolveBlock,
|
||
rejecter: @escaping RCTPromiseRejectBlock
|
||
) {
|
||
guard #available(iOS 13.0, *) else {
|
||
rejecter(
|
||
"BACKGROUND_TASK_UNAVAILABLE",
|
||
"BGTaskScheduler requires iOS 13 or later.",
|
||
nil
|
||
)
|
||
return
|
||
}
|
||
|
||
BGTaskScheduler.shared.getPendingTaskRequests { [weak self] requests in
|
||
guard let self else { return }
|
||
|
||
let mapped = requests
|
||
.filter { request in
|
||
guard let identifier = self.identifier else {
|
||
return true
|
||
}
|
||
return request.identifier == identifier
|
||
}
|
||
.map { request -> [String: Any] in
|
||
var payload: [String: Any] = [
|
||
"identifier": request.identifier
|
||
]
|
||
|
||
if let date = request.earliestBeginDate {
|
||
payload["earliestBegin"] = date.timeIntervalSince1970 * 1000
|
||
} else {
|
||
payload["earliestBegin"] = NSNull()
|
||
}
|
||
|
||
if let processingRequest = request as? BGProcessingTaskRequest {
|
||
payload["requiresNetworkConnectivity"] = processingRequest.requiresNetworkConnectivity
|
||
payload["requiresExternalPower"] = processingRequest.requiresExternalPower
|
||
payload["type"] = "processing"
|
||
} else {
|
||
payload["type"] = "refresh"
|
||
}
|
||
|
||
return payload
|
||
}
|
||
|
||
resolver(mapped)
|
||
}
|
||
}
|
||
|
||
@objc
|
||
func backgroundRefreshStatus(
|
||
_ resolver: @escaping RCTPromiseResolveBlock,
|
||
rejecter: @escaping RCTPromiseRejectBlock
|
||
) {
|
||
guard Thread.isMainThread else {
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.backgroundRefreshStatus(resolver, rejecter: rejecter)
|
||
}
|
||
return
|
||
}
|
||
|
||
let status = UIApplication.shared.backgroundRefreshStatus
|
||
switch status {
|
||
case .available:
|
||
resolver("available")
|
||
case .denied:
|
||
resolver("denied")
|
||
case .restricted:
|
||
resolver("restricted")
|
||
@unknown default:
|
||
resolver("unknown")
|
||
}
|
||
}
|
||
|
||
@objc
|
||
func simulateLaunch(
|
||
_ resolver: @escaping RCTPromiseResolveBlock,
|
||
rejecter: @escaping RCTPromiseRejectBlock
|
||
) {
|
||
guard hasListeners else {
|
||
rejecter("NO_LISTENERS", "No JS listeners registered for background events.", nil)
|
||
return
|
||
}
|
||
|
||
DispatchQueue.main.async { [weak self] in
|
||
guard let self else { return }
|
||
|
||
self.sendEvent(
|
||
withName: "BackgroundTaskBridge.execute",
|
||
body: [
|
||
"identifier": self.identifier ?? "simulated",
|
||
"timestamp": Date().timeIntervalSince1970 * 1000,
|
||
"simulated": true
|
||
]
|
||
)
|
||
resolver(["simulated": true])
|
||
}
|
||
}
|
||
|
||
// MARK: - Private helpers
|
||
|
||
@available(iOS 13.0, *)
|
||
private func scheduleTask(after delay: TimeInterval) throws {
|
||
guard let identifier else {
|
||
throw NSError(
|
||
domain: "BackgroundTaskBridge",
|
||
code: 1,
|
||
userInfo: [NSLocalizedDescriptionKey: "Identifier is missing. Configure the bridge first."]
|
||
)
|
||
}
|
||
|
||
// 取消之前的任务请求
|
||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: identifier)
|
||
|
||
// 使用 BGAppRefreshTaskRequest 而不是 BGProcessingTaskRequest
|
||
// BGAppRefreshTaskRequest 更适合定期刷新数据的场景
|
||
let request = BGAppRefreshTaskRequest(identifier: identifier)
|
||
|
||
// 设置最早开始时间
|
||
// 注意:实际执行时间由系统决定,可能会延迟
|
||
request.earliestBeginDate = Date(timeIntervalSinceNow: delay)
|
||
|
||
do {
|
||
try BGTaskScheduler.shared.submit(request)
|
||
NSLog("[BackgroundTaskBridge] 后台任务已调度,标识符: \(identifier),延迟: \(delay)秒")
|
||
} catch {
|
||
NSLog("[BackgroundTaskBridge] 调度后台任务失败: \(error.localizedDescription)")
|
||
throw error
|
||
}
|
||
}
|
||
|
||
@available(iOS 13.0, *)
|
||
private func handle(task: BGTask) {
|
||
queue.async { [weak self] in
|
||
guard let self else { return }
|
||
|
||
self.currentTask = task
|
||
NSLog("[BackgroundTaskBridge] 开始处理后台任务: \(task.identifier)")
|
||
|
||
// 设置任务过期处理器
|
||
task.expirationHandler = { [weak self] in
|
||
guard let self else { return }
|
||
NSLog("[BackgroundTaskBridge] 后台任务即将过期")
|
||
self.queue.async {
|
||
if let currentTask = self.currentTask {
|
||
currentTask.setTaskCompleted(success: false)
|
||
}
|
||
self.currentTask = nil
|
||
|
||
// 即使任务过期,也要重新调度下一次任务
|
||
self.rescheduleTask()
|
||
}
|
||
|
||
guard self.hasListeners else { return }
|
||
|
||
DispatchQueue.main.async {
|
||
self.sendEvent(
|
||
withName: "BackgroundTaskBridge.expire",
|
||
body: [
|
||
"identifier": self.identifier ?? "",
|
||
"timestamp": Date().timeIntervalSince1970 * 1000
|
||
]
|
||
)
|
||
}
|
||
}
|
||
|
||
guard self.hasListeners else {
|
||
NSLog("[BackgroundTaskBridge] 没有JS监听器,执行默认处理并重新调度")
|
||
// 即使没有JS监听器,也要执行基本的任务处理
|
||
self.executeDefaultTaskAndReschedule()
|
||
return
|
||
}
|
||
|
||
NSLog("[BackgroundTaskBridge] 发送后台任务执行事件到JS")
|
||
DispatchQueue.main.async {
|
||
self.sendEvent(
|
||
withName: "BackgroundTaskBridge.execute",
|
||
body: [
|
||
"identifier": self.identifier ?? "",
|
||
"timestamp": Date().timeIntervalSince1970 * 1000
|
||
]
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
@available(iOS 13.0, *)
|
||
private func executeDefaultTaskAndReschedule() {
|
||
// 执行默认的后台任务逻辑(当没有JS监听器时)
|
||
NSLog("[BackgroundTaskBridge] 执行默认后台任务逻辑")
|
||
|
||
// 这里可以添加一些基本的任务逻辑,比如:
|
||
// 1. 检查应用状态
|
||
// 2. 更新本地存储
|
||
// 3. 准备下次任务的数据
|
||
|
||
// 完成当前任务
|
||
if let currentTask = self.currentTask {
|
||
currentTask.setTaskCompleted(success: true)
|
||
self.currentTask = nil
|
||
}
|
||
|
||
// 重新调度下一次任务
|
||
self.rescheduleTask()
|
||
}
|
||
|
||
@available(iOS 13.0, *)
|
||
private func rescheduleTask() {
|
||
guard let identifier = self.identifier else {
|
||
NSLog("[BackgroundTaskBridge] 无法重新调度任务:标识符为空")
|
||
return
|
||
}
|
||
|
||
// 先取消之前的任务请求,避免重复
|
||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: identifier)
|
||
|
||
let request = BGAppRefreshTaskRequest(identifier: identifier)
|
||
request.earliestBeginDate = Date(timeIntervalSinceNow: self.defaultDelay)
|
||
|
||
do {
|
||
try BGTaskScheduler.shared.submit(request)
|
||
NSLog("[BackgroundTaskBridge] 已重新调度后台任务,标识符: \(identifier),延迟: \(self.defaultDelay)秒")
|
||
} catch {
|
||
NSLog("[BackgroundTaskBridge] 重新调度后台任务失败: \(error.localizedDescription)")
|
||
|
||
// 如果调度失败,尝试使用更短的延迟时间重试
|
||
let retryDelay = min(self.defaultDelay * 0.5, 5 * 60) // 最多5分钟
|
||
request.earliestBeginDate = Date(timeIntervalSinceNow: retryDelay)
|
||
|
||
do {
|
||
try BGTaskScheduler.shared.submit(request)
|
||
NSLog("[BackgroundTaskBridge] 使用重试延迟重新调度后台任务成功: \(retryDelay)秒")
|
||
} catch {
|
||
NSLog("[BackgroundTaskBridge] 重试调度后台任务也失败: \(error.localizedDescription)")
|
||
}
|
||
}
|
||
}
|
||
}
|