Files
digital-pilates/ios/OutLive/BackgroundTaskBridge.swift
richarjiang d74046498d # 分析方案
## 变更内容总结
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后台任务调度、通知推送、应用状态管理
2025-11-04 19:14:53 +08:00

468 lines
13 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?
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)")
}
}
}
}