Loading...
Loading...
Compare original and translation side by side
axiom-background-processingaxiom-background-processing-refaxiom-background-processingaxiom-background-processing-refsubmit()submit()Task never runs?
│
├─ Step 1: Check Info.plist (2 min)
│ ├─ BGTaskSchedulerPermittedIdentifiers contains EXACT identifier?
│ │ └─ NO → Add identifier, rebuild
│ ├─ UIBackgroundModes includes "fetch" or "processing"?
│ │ └─ NO → Add required mode
│ └─ Identifiers case-sensitive match code?
│ └─ NO → Fix typo, rebuild
│
├─ Step 2: Check registration timing (2 min)
│ ├─ Registered in didFinishLaunchingWithOptions?
│ │ └─ NO → Move registration before return true
│ └─ Registration before first submit()?
│ └─ NO → Ensure register() precedes submit()
│
└─ Step 3: Check app state (1 min)
├─ App swiped away from App Switcher?
│ └─ YES → No background until user opens app
└─ Background App Refresh disabled in Settings?
└─ YES → Enable or inform user任务从未运行?
│
├─ 步骤1:检查Info.plist(2分钟)
│ ├─ BGTaskSchedulerPermittedIdentifiers中是否包含完全匹配的标识符?
│ │ └─ 否 → 添加标识符,重新构建
│ ├─ UIBackgroundModes是否包含"fetch"或"processing"?
│ │ └─ 否 → 添加所需模式
│ └─ 标识符与代码是否大小写完全匹配?
│ └─ 否 → 修正拼写错误,重新构建
│
├─ 步骤2:检查注册时机(2分钟)
│ ├─ 是否在didFinishLaunchingWithOptions中注册?
│ │ └─ 否 → 将注册代码移至return true之前
│ └─ 注册是否在首次调用submit()之前完成?
│ └─ 否 → 确保register()在submit()之前执行
│
└─ 步骤3:检查应用状态(1分钟)
├─ 应用是否从应用切换器中被划走?
│ └─ 是 → 除非用户重新打开应用,否则无法在后台运行
└─ 设置中是否禁用了后台应用刷新?
└─ 是 → 启用该功能或告知用户| Approach | Time | Success Rate |
|---|---|---|
| Check Info.plist + registration | 5 min | 70% (catches most issues) |
| Add console logging | 15 min | 90% |
| LLDB simulate launch | 5 min | 95% (confirms handler works) |
| Random code changes | 2+ hours | Low |
| 方法 | 耗时 | 成功率 |
|---|---|---|
| 检查Info.plist + 注册配置 | 5分钟 | 70%(能排查大多数问题) |
| 添加控制台日志 | 15分钟 | 90% |
| LLDB模拟启动 | 5分钟 | 95%(确认处理器工作正常) |
| 随机修改代码 | 2小时以上 | 低 |
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.yourapp.refresh"]e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.yourapp.refresh"]Task terminates early?
│
├─ Step 1: Check expiration handler (1 min)
│ ├─ Expiration handler set FIRST in handler?
│ │ └─ NO → Move to very first line
│ └─ Expiration handler actually cancels work?
│ └─ NO → Add cancellation logic
│
├─ Step 2: Check setTaskCompleted (2 min)
│ ├─ Called in success path?
│ ├─ Called in failure path?
│ ├─ Called after expiration?
│ └─ ANY path missing → Task never signals completion
│
├─ Step 3: Check work duration (2 min)
│ ├─ BGAppRefreshTask work > 30 seconds?
│ │ └─ YES → Chunk work or use BGProcessingTask
│ └─ BGProcessingTask work > system limit?
│ └─ YES → Save progress, resume on next launch任务提前终止?
│
├─ 步骤1:检查过期处理器(1分钟)
│ ├─ 是否在处理器的第一行设置过期处理器?
│ │ └─ 否 → 将其移至第一行
│ └─ 过期处理器是否实际执行了取消工作的逻辑?
│ └─ 否 → 添加取消逻辑
│
├─ 步骤2:检查setTaskCompleted调用(2分钟)
│ ├─ 成功路径中是否调用了该方法?
│ ├─ 失败路径中是否调用了该方法?
│ ├─ 过期后是否调用了该方法?
│ └─ 任何路径未调用 → 任务从未发出完成信号
│
├─ 步骤3:检查工作时长(2分钟)
│ ├─ BGAppRefreshTask的工作时长是否超过30秒?
│ │ └─ 是 → 拆分工作或使用BGProcessingTask
│ └─ BGProcessingTask的工作时长是否超过系统限制?
│ └─ 是 → 保存进度,下次启动时恢复| Cause | Fix |
|---|---|
| Missing expiration handler | Set handler as first line |
| setTaskCompleted not called | Add to ALL code paths |
| Work takes too long | Chunk and checkpoint |
| Network timeout > task time | Use background URLSession |
| Async callback after expiration | Check shouldContinue flag |
| 原因 | 修复方案 |
|---|---|
| 缺少过期处理器 | 将处理器设置为第一行代码 |
| 未调用setTaskCompleted | 在所有代码路径中添加调用 |
| 工作耗时过长 | 拆分任务并设置检查点 |
| 网络超时超过任务时长 | 使用后台URLSession |
| 过期后执行异步回调 | 检查shouldContinue标志 |
// First simulate launch
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.yourapp.refresh"]
// Then force expiration
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.yourapp.refresh"]// 首先模拟启动
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.yourapp.refresh"]
// 然后强制触发过期
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.yourapp.refresh"]didFinishDownloadingTodidFinishDownloadingToURLSession delegate not called?
│
├─ Step 1: Check session configuration (2 min)
│ ├─ Using URLSessionConfiguration.background()?
│ │ └─ NO → Must use background config
│ ├─ Session identifier unique?
│ │ └─ NO → Use unique bundle-prefixed ID
│ └─ sessionSendsLaunchEvents = true?
│ └─ NO → Set for app relaunch on completion
│
├─ Step 2: Check AppDelegate handler (2 min)
│ ├─ handleEventsForBackgroundURLSession implemented?
│ │ └─ NO → Required for session events
│ └─ Completion handler stored and called later?
│ └─ NO → Store handler, call after events processed
│
└─ Step 3: Check delegate assignment (1 min)
├─ Session created with delegate?
└─ Delegate not nil when task completes?URLSession代理未被调用?
│
├─ 步骤1:检查会话配置(2分钟)
│ ├─ 是否使用URLSessionConfiguration.background()?
│ │ └─ 否 → 必须使用后台配置
│ ├─ 会话标识符是否唯一?
│ │ └─ 否 → 使用带Bundle前缀的唯一ID
│ └─ sessionSendsLaunchEvents是否设为true?
│ └─ 否 → 设置为true以在完成时重启应用
│
├─ 步骤2:检查AppDelegate处理器(2分钟)
│ ├─ 是否实现了handleEventsForBackgroundURLSession?
│ │ └─ 否 → 该方法是处理会话事件的必需方法
│ └─ 是否保存并在后续调用了完成处理器?
│ └─ 否 → 保存处理器,处理完事件后再调用
│
└─ 步骤3:检查代理赋值(1分钟)
├─ 创建会话时是否指定了代理?
└─ 任务完成时代理是否不为nil?// Store completion handler
var backgroundSessionCompletionHandler: (() -> Void)?
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
backgroundSessionCompletionHandler = completionHandler
}
// Call after all events processed
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
self.backgroundSessionCompletionHandler?()
self.backgroundSessionCompletionHandler = nil
}
}// 保存完成处理器
var backgroundSessionCompletionHandler: (() -> Void)?
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
backgroundSessionCompletionHandler = completionHandler
}
// 处理完所有事件后调用
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
self.backgroundSessionCompletionHandler?()
self.backgroundSessionCompletionHandler = nil
}
}Works in dev, not prod?
│
├─ Step 1: Check system constraints (3 min)
│ ├─ Low Power Mode enabled?
│ │ └─ Check ProcessInfo.isLowPowerModeEnabled
│ ├─ Background App Refresh disabled?
│ │ └─ Check UIApplication.backgroundRefreshStatus
│ └─ Battery < 20%?
│ └─ System pauses discretionary work
│
├─ Step 2: Check app state (2 min)
│ ├─ App force-quit from App Switcher?
│ │ └─ YES → No background until foreground launch
│ └─ App recently used?
│ └─ Rarely used apps get lower priority
│
├─ Step 3: Check build differences (3 min)
│ ├─ Debug vs Release optimization differences?
│ ├─ #if DEBUG code excluding production?
│ └─ Different bundle identifier in release?
│
└─ Step 4: Add production logging (2 min)
└─ Log task schedule/launch/complete to analytics开发环境正常,生产环境异常?
│
├─ 步骤1:检查系统限制(3分钟)
│ ├─ 是否启用了低电量模式?
│ │ └─ 检查ProcessInfo.isLowPowerModeEnabled
│ ├─ 是否禁用了后台应用刷新?
│ │ └─ 检查UIApplication.backgroundRefreshStatus
│ └─ 电量是否低于20%?
│ └─ 系统会暂停可自由支配的工作
│
├─ 步骤2:检查应用状态(2分钟)
│ ├─ 应用是否从应用切换器中被强制退出?
│ │ └─ 是 → 除非应用前台启动,否则无法在后台运行
│ └─ 应用是否被用户频繁使用?
│ └─ 很少使用的应用优先级更低
│
├─ 步骤3:检查构建差异(3分钟)
│ ├─ Debug与Release版本的优化设置是否不同?
│ ├─ 是否有#if DEBUG代码在生产环境中被排除?
│ └─ 发布版本的Bundle标识符是否不同?
│
└─ 步骤4:添加生产环境日志(2分钟)
└─ 将任务的调度/启动/完成情况记录到分析工具中| Factor | Check |
|---|---|
| Critically Low Battery | Battery < 20%? |
| Low Power Mode | ProcessInfo.isLowPowerModeEnabled |
| App Usage | User opens app frequently? |
| App Switcher | App NOT swiped away? |
| Background App Refresh | Settings enabled? |
| System Budgets | Many recent background launches? |
| Rate Limiting | Requests too frequent? |
| 因素 | 检查项 |
|---|---|
| 电量极低 | 电量是否低于20%? |
| 低电量模式 | ProcessInfo.isLowPowerModeEnabled |
| 应用使用频率 | 用户是否频繁打开应用? |
| 应用切换器状态 | 应用是否未被划走? |
| 后台应用刷新 | 设置中是否启用? |
| 系统预算 | 近期是否有大量后台启动? |
| 速率限制 | 请求是否过于频繁? |
func scheduleRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.app.refresh")
do {
try BGTaskScheduler.shared.submit(request)
Analytics.log("background_task_scheduled")
} catch {
Analytics.log("background_task_schedule_failed", error: error)
}
}
func handleRefresh(task: BGAppRefreshTask) {
Analytics.log("background_task_started")
// ... work ...
Analytics.log("background_task_completed")
task.setTaskCompleted(success: true)
}func scheduleRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.app.refresh")
do {
try BGTaskScheduler.shared.submit(request)
Analytics.log("background_task_scheduled")
} catch {
Analytics.log("background_task_schedule_failed", error: error)
}
}
func handleRefresh(task: BGAppRefreshTask) {
Analytics.log("background_task_started")
// ... 执行工作 ...
Analytics.log("background_task_completed")
task.setTaskCompleted(success: true)
}Inconsistent scheduling?
│
├─ Step 1: Understand earliestBeginDate (2 min)
│ ├─ This is MINIMUM delay, not scheduled time
│ │ └─ System runs when convenient AFTER this date
│ └─ Set too far in future (> 1 week)?
│ └─ System may skip task entirely
│
├─ Step 2: Check scheduling pattern (2 min)
│ ├─ Scheduling same task multiple times?
│ │ └─ Call getPendingTaskRequests to check
│ └─ Scheduling in handler for continuity?
│ └─ Required for continuous refresh
│
└─ Step 3: Understand system behavior (1 min)
├─ BGAppRefreshTask runs based on USER patterns
│ └─ User rarely opens app = rare runs
└─ BGProcessingTask runs when charging
└─ User doesn't charge overnight = no runs调度不一致?
│
├─ 步骤1:理解earliestBeginDate(2分钟)
│ ├─ 这是最小延迟时间,而非固定调度时间
│ │ └─ 系统会在该时间之后的合适时机运行任务
│ └─ 是否设置为过远的未来时间(超过1周)?
│ └─ 是 → 系统可能会完全跳过该任务
│
├─ 步骤2:检查调度逻辑(2分钟)
│ ├─ 是否多次调度同一个任务?
│ │ └─ 调用getPendingTaskRequests检查
│ └─ 是否在处理器中调度任务以保持连续性?
│ └─ 是 → 这是实现持续刷新的必需操作
│
└─ 步骤3:理解系统行为(1分钟)
├─ BGAppRefreshTask基于用户使用模式运行
│ └─ 用户很少打开应用 → 任务运行频率低
└─ BGProcessingTask在充电时运行
└─ 用户夜间不充电 → 任务不运行| Task Type | Scheduling Behavior |
|---|---|
| BGAppRefreshTask | Runs before predicted app usage times |
| BGProcessingTask | Runs when charging + idle (typically overnight) |
| Silent Push | Rate-limited; 14 pushes may = 7 launches |
| 任务类型 | 调度行为 |
|---|---|
| BGAppRefreshTask | 在预测的用户打开应用时间之前运行 |
| BGProcessingTask | 在充电且设备空闲时运行(通常是夜间) |
| 静默推送 | 受速率限制;14次推送可能仅触发7次启动 |
Crash on background launch?
│
├─ Step 1: Check launch initialization (2 min)
│ ├─ UI setup before task handler?
│ │ └─ Background launch may not have UI context
│ ├─ Accessing files before first unlock?
│ │ └─ Use completeUntilFirstUserAuthentication protection
│ └─ Force unwrapping optionals that may be nil?
│ └─ Guard against nil in background context
│
├─ Step 2: Check handler safety (2 min)
│ ├─ Handler captures self strongly?
│ │ └─ Use [weak self] to prevent retain cycles
│ └─ Handler accesses UI on non-main thread?
│ └─ Dispatch UI work to main queue
│
└─ Step 3: Check data protection (1 min)
└─ Files accessible when device locked?
└─ Use .completeUnlessOpen or .completeUntilFirstUserAuthentication后台启动时崩溃?
│
├─ 步骤1:检查启动初始化逻辑(2分钟)
│ ├─ 是否在任务处理器之前进行UI设置?
│ │ └─ 后台启动可能没有UI上下文
│ ├─ 是否在设备首次解锁前访问文件?
│ │ └─ 使用completeUntilFirstUserAuthentication保护级别
│ └─ 是否强制解包可能为nil的可选值?
│ └─ 在后台上下文中要防范nil值
│
├─ 步骤2:检查处理器安全性(2分钟)
│ ├─ 处理器是否强引用self?
│ │ └─ 使用[weak self]避免循环引用
│ └─ 处理器是否在非主线程访问UI?
│ └─ 将UI工作调度到主队列
│
└─ 步骤3:检查数据保护(1分钟)
└─ 设备锁定时文件是否可访问?
└─ 使用.completeUnlessOpen或.completeUntilFirstUserAuthentication// Set appropriate protection when creating files
try data.write(to: url, options: .completeFileProtectionUntilFirstUserAuthentication)
// Or configure in entitlements for entire app// 创建文件时设置合适的保护级别
try data.write(to: url, options: .completeFileProtectionUntilFirstUserAuthentication)
// 或在 entitlements 中为整个应用配置BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.app.refresh",
using: nil
) { [weak self] task in
guard let self = self else {
task.setTaskCompleted(success: false)
return
}
// Don't access UI
// Use background-safe APIs only
self.performBackgroundWork(task: task)
}BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.app.refresh",
using: nil
) { [weak self] task in
guard let self = self else {
task.setTaskCompleted(success: false)
return
}
// 不要访问UI
// 仅使用后台安全的API
self.performBackgroundWork(task: task)
}Task runs multiple times?
│
├─ Step 1: Check scheduling logic (2 min)
│ ├─ Scheduling on every app launch?
│ │ └─ Check getPendingTaskRequests first
│ ├─ Scheduling in handler AND elsewhere?
│ │ └─ Consolidate to single location
│ └─ Using same identifier for different purposes?
│ └─ Use unique identifiers per task type
│
├─ Step 2: Check for duplicate submissions (2 min)
│ └─ Multiple submit() calls queued?
│ └─ System may batch into single execution
│
└─ Step 3: Check handler execution (1 min)
└─ setTaskCompleted called promptly?
└─ Delay may cause system to think task hung任务多次运行?
│
├─ 步骤1:检查调度逻辑(2分钟)
│ ├─ 是否每次应用启动都调度任务?
│ │ └─ 先调用getPendingTaskRequests检查
│ ├─ 是否同时在处理器和其他地方调度任务?
│ │ └─ 统一到单一位置调度
│ └─ 是否将同一标识符用于不同用途?
│ └─ 为不同类型的任务使用唯一标识符
│
├─ 步骤2:检查重复提交(2分钟)
│ └─ 是否有多个submit()调用被排队?
│ └─ 系统可能会将其合并为单次执行
│
└─ 步骤3:检查处理器执行情况(1分钟)
└─ 是否及时调用了setTaskCompleted?
└─ 延迟调用可能导致系统认为任务挂起func scheduleRefreshIfNeeded() {
BGTaskScheduler.shared.getPendingTaskRequests { requests in
let alreadyScheduled = requests.contains {
$0.identifier == "com.app.refresh"
}
if !alreadyScheduled {
self.scheduleRefresh()
}
}
}func scheduleRefreshIfNeeded() {
BGTaskScheduler.shared.getPendingTaskRequests { requests in
let alreadyScheduled = requests.contains {
$0.identifier == "com.app.refresh"
}
if !alreadyScheduled {
self.scheduleRefresh()
}
}
}// All background task events
subsystem:com.apple.backgroundtaskscheduler
// Specific to your app
subsystem:com.apple.backgroundtaskscheduler message:"com.yourapp"// 所有后台任务事件
subsystem:com.apple.backgroundtaskscheduler
// 仅针对你的应用
subsystem:com.apple.backgroundtaskscheduler message:"com.yourapp"