Files
digital-pilates/ios/OutLive/BackgroundTaskBridge.swift
richarjiang 9b1a40cea3 feat(background-task): 实现原生与JS层的任务同步机制
解决后台任务在JS监听器未就绪时丢失的问题。新增任务缓存队列,当检测到无JS监听器时将任务暂存,并启动20秒超时计时器等待JS初始化完成。JS层通过markJSReady接口通知原生层准备就绪,触发缓存任务的立即执行。超时后自动切换到默认处理逻辑,确保任务不丢失。
2025-11-06 09:20:52 +08:00

583 lines
17 KiB
Swift
Raw 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)")
// 使 BGAppRefreshTaskRequest BGProcessingTaskRequest
// BGAppRefreshTaskRequest
let request = BGAppRefreshTaskRequest(identifier: identifier)
//
//
//
request.earliestBeginDate = Date(timeIntervalSinceNow: delay)
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] 开始处理后台任务: \(task.identifier)")
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")
self.emitTaskToJS(payload: payload)
}
}
@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 = 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)")
}
}
}
private func cacheTaskForLater(payload: [String: Any]) {
waitingForJSListeners = true
pendingTaskPayload = payload
pendingTaskTimeoutWorkItem?.cancel()
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)
}
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 }
self.sendEvent(
withName: "BackgroundTaskBridge.execute",
body: payload
)
}
}
}