Files
digital-pilates/ios/OutLive/BackgroundTaskBridge.swift
richarjiang d282abd146 feat(background): 增强iOS后台任务系统,添加processing任务类型支持
- 添加新的processing任务标识符到iOS配置文件
- 重构BackgroundTaskBridge支持不同任务类型(refresh/processing)
- 增强后台任务日志记录和调试信息
- 修复任务类型配置不匹配问题
- 改进任务调度逻辑和错误处理机制
- 添加任务执行时间戳记录用于调试
- 移除notification-settings中未使用的AuthGuard依赖
2025-11-13 17:12:57 +08:00

621 lines
19 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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?
private let pendingTaskWaitTimeout: TimeInterval = 20
private var waitingForJSListeners = false
private var pendingTaskPayload: [String: Any]?
private var pendingTaskTimeoutWorkItem: DispatchWorkItem?
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
queue.async { [weak self] in
self?.emitPendingTaskIfPossible()
}
}
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
) {
#if targetEnvironment(simulator)
//
NSLog("[BackgroundTaskBridge] ⚠️ 后台任务在模拟器上不完全支持")
NSLog("[BackgroundTaskBridge] 请在真机上测试后台任务功能")
rejecter(
"SIMULATOR_NOT_SUPPORTED",
"后台任务功能在模拟器上不完全支持。请在真机上测试。\n注意:这不是错误,是 iOS 模拟器的正常限制。",
nil
)
return
#else
guard hasListeners else {
NSLog("[BackgroundTaskBridge] ⚠️ 没有 JS 监听器注册")
rejecter(
"NO_LISTENERS",
"没有 JS 监听器注册后台事件。请确保应用已完全初始化。",
nil
)
return
}
NSLog("[BackgroundTaskBridge] 模拟触发后台任务...")
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
]
)
NSLog("[BackgroundTaskBridge] ✅ 模拟后台任务已触发")
resolver(["simulated": true])
}
#endif
}
@objc
func markJSReady(
_ resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock
) {
queue.async { [weak self] in
guard let self else { return }
let hadPendingTask = self.waitingForJSListeners && self.pendingTaskPayload != nil
self.emitPendingTaskIfPossible()
resolver([
"pendingTaskFlushed": hadPendingTask
])
}
}
// 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.getPendingTaskRequests { requests in
let existingTasks = requests.filter { $0.identifier == identifier }
NSLog("[BackgroundTaskBridge] 当前待处理任务数: \(existingTasks.count)")
}
//
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: identifier)
NSLog("[BackgroundTaskBridge] 已取消之前的任务请求: \(identifier)")
//
// processing 使 BGProcessingTaskRequestfetch 使 BGAppRefreshTaskRequest
let request: BGTaskRequest
switch kind {
case .processing:
request = BGProcessingTaskRequest(identifier: identifier)
case .refresh:
request = BGAppRefreshTaskRequest(identifier: identifier)
}
//
//
//
if let appRefreshRequest = request as? BGAppRefreshTaskRequest {
appRefreshRequest.earliestBeginDate = Date(timeIntervalSinceNow: delay)
} else if let processingRequest = request as? BGProcessingTaskRequest {
processingRequest.earliestBeginDate = Date(timeIntervalSinceNow: delay)
processingRequest.requiresNetworkConnectivity = requiresNetworkConnectivity
processingRequest.requiresExternalPower = requiresExternalPower
}
do {
try BGTaskScheduler.shared.submit(request)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm:ss"
let earliestTime = dateFormatter.string(from: request.earliestBeginDate ?? Date())
NSLog("[BackgroundTaskBridge] ✅ 后台任务已调度成功")
NSLog("[BackgroundTaskBridge] - 标识符: \(identifier)")
NSLog("[BackgroundTaskBridge] - 延迟: \(Int(delay))秒 (\(Int(delay/60))分钟)")
NSLog("[BackgroundTaskBridge] - 最早执行时间: \(earliestTime)")
NSLog("[BackgroundTaskBridge] - 注意: 实际执行时间由系统决定")
} catch {
NSLog("[BackgroundTaskBridge] ❌ 调度后台任务失败: \(error.localizedDescription)")
//
if let bgError = error as NSError? {
NSLog("[BackgroundTaskBridge] - 错误域: \(bgError.domain)")
NSLog("[BackgroundTaskBridge] - 错误码: \(bgError.code)")
NSLog("[BackgroundTaskBridge] - 错误信息: \(bgError.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] ===== 开始处理后台任务 =====")
NSLog("[BackgroundTaskBridge] 任务标识符: \(task.identifier)")
NSLog("[BackgroundTaskBridge] 任务类型: \(type(of: task))")
NSLog("[BackgroundTaskBridge] 当前时间: \(Date().description)")
if self.identifier == nil {
self.identifier = task.identifier
NSLog("[BackgroundTaskBridge] 使用任务标识符初始化 identifier: \(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
]
)
}
}
let payload: [String: Any] = [
"identifier": self.identifier ?? task.identifier,
"timestamp": Date().timeIntervalSince1970 * 1000
]
guard self.hasListeners else {
NSLog("[BackgroundTaskBridge] 暂无 JS 监听器,等待 JS 初始化 (最多 \(Int(self.pendingTaskWaitTimeout)) 秒)")
self.cacheTaskForLater(payload: payload)
return
}
NSLog("[BackgroundTaskBridge] 准备发送后台任务执行事件到JS")
NSLog("[BackgroundTaskBridge] hasListeners: \(self.hasListeners)")
self.emitTaskToJS(payload: payload)
NSLog("[BackgroundTaskBridge] ✅ 事件已发送到JS层")
}
}
@available(iOS 13.0, *)
private func executeDefaultTaskAndReschedule() {
waitingForJSListeners = false
pendingTaskPayload = nil
pendingTaskTimeoutWorkItem?.cancel()
pendingTaskTimeoutWorkItem = nil
// 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: BGTaskRequest
switch self.kind {
case .processing:
request = BGProcessingTaskRequest(identifier: identifier)
if let processingRequest = request as? BGProcessingTaskRequest {
processingRequest.earliestBeginDate = Date(timeIntervalSinceNow: self.defaultDelay)
processingRequest.requiresNetworkConnectivity = self.requiresNetworkConnectivity
processingRequest.requiresExternalPower = self.requiresExternalPower
}
case .refresh:
request = BGAppRefreshTaskRequest(identifier: identifier)
if let appRefreshRequest = request as? BGAppRefreshTaskRequest {
appRefreshRequest.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)")
}
}
}
private func cacheTaskForLater(payload: [String: Any]) {
waitingForJSListeners = true
pendingTaskPayload = payload
pendingTaskTimeoutWorkItem?.cancel()
NSLog("[BackgroundTaskBridge] 缓存后台任务等待JS监听器...")
NSLog("[BackgroundTaskBridge] 等待超时时间: \(Int(pendingTaskWaitTimeout))")
let timeoutItem = DispatchWorkItem { [weak self] in
guard let self else { return }
guard self.waitingForJSListeners else { return }
NSLog("[BackgroundTaskBridge] ⚠️ 等待 JS 监听器超时,执行默认处理")
self.waitingForJSListeners = false
self.pendingTaskPayload = nil
self.pendingTaskTimeoutWorkItem = nil
self.executeDefaultTaskAndReschedule()
}
pendingTaskTimeoutWorkItem = timeoutItem
queue.asyncAfter(deadline: .now() + pendingTaskWaitTimeout, execute: timeoutItem)
NSLog("[BackgroundTaskBridge] 已设置超时计时器")
}
private func emitPendingTaskIfPossible() {
guard hasListeners else { return }
guard waitingForJSListeners, let payload = pendingTaskPayload else { return }
pendingTaskTimeoutWorkItem?.cancel()
pendingTaskTimeoutWorkItem = nil
waitingForJSListeners = false
pendingTaskPayload = nil
NSLog("[BackgroundTaskBridge] JS 监听器已就绪,发送缓存的后台任务事件")
emitTaskToJS(payload: payload)
}
private func emitTaskToJS(payload: [String: Any]) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
NSLog("[BackgroundTaskBridge] 正在发送事件到JS层...")
self.sendEvent(
withName: "BackgroundTaskBridge.execute",
body: payload
)
NSLog("[BackgroundTaskBridge] ✅ 事件发送成功")
}
}
}