关于 axiom-push-notifications-diag
面向开发者的 iOS 推送通知故障排查技能,涵盖令牌、配置、负载与投递问题,并强调先核验权限和令牌注册。
主要功能
- 01核验权限与令牌
- 02验证 APNs 负载
- 03检查通知授权状态
- 04按决策树排查问题
- 05模拟器与 FCM 诊断
适用场景
- 01排查未收到通知
- 02分析开发与生产环境差异
- 03诊断静默或富媒体推送
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-push-notifications-diag安装后在 Claude Code 中输入/axiom-push-notifications-diag确认已加载。
详细介绍
推送通知诊断
系统地排查推送通知故障:通知缺失、令牌注册错误、环境不匹配、静默推送节流以及服务扩展问题。
概述
核心原则:推送通知无法正常工作时,问题通常出在:
- 令牌/注册故障(从未注册、格式错误、已过期)——30%
- 权限/配置文件不匹配(缺少功能、环境错误)——25%
- 负载结构错误(缺少键、类型错误、JSON 无效)——15%
- 专注模式/中断抑制(iOS 15+ 过滤、临时授权)——15%
- 服务扩展故障(超时、崩溃、缺少 mutable-content)——10%
- 投递时机/节流(静默推送额度、APNs 合并)——5%
在排查负载或投递逻辑之前,务必先验证权限和令牌注册。
危险信号
表明存在推送特有问题的症状:
| 症状 | 可能原因 |
|---|---|
| 完全没有通知 | 缺少推送通知功能或配置文件 |
| 开发环境可用,生产环境不可用 | 使用生产令牌向 APNs 沙盒发送(或反之) |
| 在 Simulator 上令牌注册失败 | 这是预期行为——Simulator 无法注册远程通知 |
| 通知出现但没有声音 | 授权选项或负载中缺少 .sound |
| 富通知显示为纯文本 | 负载中缺少 mutable-content: 1 |
| 通知中未显示图像 | 服务扩展静默失败——检查 serviceExtensionTimeWillExpire |
| 静默推送未唤醒应用 | 系统节流(约每小时 2-3 次),或应用已被用户强制退出 |
| iOS 更新后通知停止 | iOS 15+ 默认启用了专注模式;检查中断级别 |
| 徽标显示数量错误 | 发送多条通知时未显式重置徽标数量 |
| 未显示操作 | 负载中的类别标识符与已注册类别不匹配 |
| 通知出现两次 | 同一事件同时安排了本地通知和远程通知 |
| FCM 在 Android 上可用,但在 iOS 上不可用 | Firebase Console 中缺少 APNs 身份验证密钥上传 |
反合理化
| 合理化说法 | 为什么行不通 | 时间成本 |
|---|---|---|
| “昨天还能用,所以授权配置没问题” | 签名变更时,配置描述文件会重新生成。务必重新验证。 | 配置描述文件丢失推送能力后,调试代码需花费 30-60 分钟 |
| “服务端说他们的负载没问题” | 55% 的推送失败发生在客户端(授权配置 + 令牌)。使用 curl 独立验证。 | 在有人检查之前,互相推诿 1-2 小时 |
| “我跳过令牌验证,错误显然出在负载上” | 环境不匹配的令牌是“开发环境正常,生产环境不行”的首要原因。 | 使用有效负载向无效令牌发送消息,调试需花费 30 分钟以上 |
| “专注模式无关紧要,我们使用默认中断级别” | 默认级别(active)会被专注模式过滤。只有 time-sensitive 和 critical 能突破限制。 | 为本可在负载层修复的问题花费数小时添加代码变通方案 |
| “静默推送很可靠,我们用它来同步” | 系统会将其限制在每小时约 2-3 次,并忽略被强制退出的应用。它只是一个提示,并非保证。 | 静默推送无法维持实时同步时,需要重做架构 |
| “服务扩展已配置好,所以富通知应该能正常工作” | 扩展需要正确的 Bundle ID 后缀、负载中的 mutable-content,并且必须在 30 秒内完成。 | 缺少这三项前提中的任意一项时,排查需花费 30 分钟以上 |
| “FCM 会处理一切,我不需要了解 APNs” | FCM 封装了 APNs。令牌类型混淆、未上传 p8 密钥,以及 swizzling 冲突都是 APNs 层面的问题。 | 问题出在 APNs 配置,却花数小时调试 FCM |
| “我先在 Simulator 上测试” | Simulator 无法注册远程通知。没有 APNs 令牌,就无法进行真正的推送测试。 | 浪费一个测试周期后才发现 Simulator 的限制 |
| “我来重写通知处理程序” | 80% 的推送失败是配置问题(授权配置、令牌、环境),而非代码问题。 | 配置仍有问题,却花数小时重写正常工作的代码 |
| “这在 iOS 17 上能用,问题肯定出在我们的代码里” | 每个 iOS 版本都会改变专注模式默认设置、中断过滤和临时授权行为。 | 修复本应通过修改负载或设置完成,却在调试代码 |
必须先完成的步骤
调查代码之前,请运行以下诊断:
步骤 1:验证推送通知授权配置
security cms -D -i path/to/embedded.mobileprovision | grep -A1 "aps-environment"
预期输出:
- ✅
<string>development</string>或<string>production</string>→ 授权配置项存在 - ❌ 没有 aps-environment 键 → 未在 Xcode 中启用推送通知功能
查找配置描述文件的方法:
# For installed app on device
find ~/Library/Developer/Xcode/DerivedData -name "embedded.mobileprovision" -newer . 2>/dev/null | head -3
步骤 2:检查令牌注册
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
print("✅ APNs token: \(token)")
print("✅ Token length: \(token.count) chars")
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("❌ Registration failed: \(error.localizedDescription)")
}
预期输出:
- ✅ 64 个字符的十六进制令牌 → 注册成功
- ❌ "no valid aps-environment entitlement" → 功能配置错误
- ❌ 完全没有触发回调 → 从未调用
registerForRemoteNotifications()
重要:两个回调都必须位于 AppDelegate,而不是 SceneDelegate。SwiftUI 应用需要 @UIApplicationDelegateAdaptor。
步骤 3:使用 curl 验证负载
curl -v \
--header "apns-topic: com.your.bundle.id" \
--header "apns-push-type: alert" \
--header "authorization: bearer $JWT_TOKEN" \
--data '{"aps":{"alert":{"title":"Test","body":"Hello"}}}' \
--http2 https://api.sandbox.push.apple.com/3/device/$DEVICE_TOKEN
预期输出:
- ✅ HTTP/2 200 → APNs 已接受负载
- ❌ 400 BadDeviceToken → 令牌格式错误或已过期
- ❌ 403 ExpiredProviderToken → JWT 已超过 1 小时
- ❌ 403 InvalidProviderToken → 密钥 ID、团队 ID 或密钥错误
- ❌ 410 Unregistered → 应用已卸载或令牌已失效
- ❌ 413 PayloadTooLarge → 超过 4096 字节
步骤 4:检查授权状态
let settings = await UNUserNotificationCenter.current().notificationSettings()
print("Authorization: \(settings.authorizationStatus.rawValue)")
print("Alert: \(settings.alertSetting.rawValue)")
print("Sound: \(settings.soundSetting.rawValue)")
print("Badge: \(settings.badgeSetting.rawValue)")
预期输出:
- ✅ authorizationStatus = 2 → 已授权
- ⚠️ authorizationStatus = 3 → 临时授权(会在通知中心静默显示)
- ❌ authorizationStatus = 1 → 用户已拒绝
- ❌ authorizationStatus = 0 → 尚未确定(从未请求)
决策树
决策树 1:收不到任何通知
Not receiving any notifications?
│
├─ Check Step 1 (entitlements)
│ ├─ No aps-environment key?
│ │ └─ Enable Push Notifications in Signing & Capabilities → DONE
│ └─ aps-environment present → continue
│
├─ Check Step 2 (token registration)
│ ├─ didFailToRegister called?
│ │ ├─ "no valid aps-environment" → Regenerate provisioning profile
│ │ └─ Other error → Check network, device (not Simulator)
│ ├─ Neither callback fires?
│ │ └─ Verify registerForRemoteNotifications() called after app launch
│ └─ Token received → continue
│
├─ Check Step 3 (payload delivery)
│ ├─ HTTP 200 but no notification?
│ │ └─ Check Step 4 (authorization status)
│ ├─ 400 BadDeviceToken?
│ │ └─ Token expired or wrong environment → Re-register
│ └─ 403/410 error?
│ └─ Fix auth credentials or re-register device
│
└─ Check Step 4 (user authorization)
├─ Status: denied?
│ └─ User must enable in Settings → Show settings prompt
├─ Status: notDetermined?
│ └─ Call requestAuthorization() → Was never requested
└─ Status: authorized but still no notifications?
└─ Check Focus mode, Do Not Disturb, notification grouping
决策树 2:开发环境正常,生产环境不行
Works in development, fails in production?
│
├─ APNs endpoint correct?
│ ├─ Dev: api.sandbox.push.apple.com
│ └─ Prod: api.push.apple.com
│ └─ Using sandbox endpoint with production build? → Switch endpoint
│
├─ Token environment matches?
│ ├─ Dev and production tokens are DIFFERENT
│ │ └─ Server storing dev token, sending to prod APNs? → Re-register on prod build
│ └─ Server distinguishes token environments? → Add environment flag to token storage
│
├─ Auth method correct?
│ ├─ .p8 key (token-based)?
│ │ └─ Same key works for both environments ✅
│ └─ .p12 certificate?
│ ├─ Dev cert → Only works with sandbox
│ └─ Prod cert → Only works with production
│ └─ Wrong cert for environment? → Generate correct certificate
│
└─ Using FCM?
├─ APNs auth key (.p8) uploaded to Firebase Console?
│ └─ Missing? → Upload in Project Settings > Cloud Messaging
└─ Key uploaded but wrong Team ID?
└─ Verify Team ID matches Apple Developer account
决策树 3:静默通知无法唤醒应用
Silent push not waking app?
│
├─ Payload correct?
│ ├─ Has "content-available": 1 in aps?
│ │ └─ Missing? → Add to aps dictionary
│ ├─ Has NO "alert", "badge", or "sound" in aps?
│ │ └─ Has alert? → Not a silent push; system treats as visible notification
│ └─ Payload valid → continue
│
├─ Headers correct?
│ ├─ apns-push-type: background?
│ │ └─ Missing or wrong? → Must be "background" for silent push
│ └─ apns-priority: 5?
│ └─ Using 10? → Silent push MUST use priority 5
│
├─ Background mode enabled?
│ ├─ "Remote notifications" checked in Background Modes capability?
│ │ └─ Missing? → Enable in Signing & Capabilities
│ └─ application(_:didReceiveRemoteNotification:fetchCompletionHandler:) implemented?
│ └─ Missing? → Implement the delegate method
│
├─ App state?
│ ├─ Force-quit by user (swiped up)?
│ │ └─ System will NOT wake force-quit apps for silent push
│ └─ Suspended or background?
│ └─ Should wake — continue debugging
│
└─ System throttling?
├─ Budget: ~2-3 silent pushes per hour
│ └─ Exceeding? → Reduce frequency, batch updates
└─ Device in Low Power Mode?
└─ Further reduces background execution budget
决策树 4:富通知缺少媒体内容
Rich notification not showing image/video?
│
├─ Payload has mutable-content: 1?
│ └─ Missing? → Required for Notification Service Extension to fire
│
├─ Notification Service Extension target exists?
│ ├─ Missing? → File > New > Target > Notification Service Extension
│ └─ Exists → continue
│
├─ Extension bundle ID correct?
│ ├─ Must be: {app-bundle-id}.{extension-name}
│ │ Example: com.myapp.NotificationService
│ └─ Wrong prefix? → Fix bundle ID to match parent app
│
├─ Download completing in time?
│ ├─ Extension has ~30 seconds to modify notification
│ │ └─ Large file? → Use thumbnail URL, not full resolution
│ └─ serviceExtensionTimeWillExpire called?
│ └─ Must deliver bestAttemptContent with fallback text
│
├─ Attachment created correctly?
│ ├─ File written to disk before creating UNNotificationAttachment?
│ │ └─ Must write to tmp directory, then create attachment from file URL
│ └─ File type supported?
│ ├─ Images: JPEG, GIF, PNG (max 10MB)
│ ├─ Audio: AIFF, WAV, MP3, M4A (max 5MB)
│ └─ Video: MPEG, MPEG-2, MP4, AVI (max 50MB)
│
└─ App groups configured?
└─ Extension and app share data via App Groups?
└─ Missing? → Add same App Group to both targets
决策树 5:实时活动无法通过推送更新
Live Activity not updating from push?
│
├─ APNs topic correct?
│ ├─ Must be: {bundleID}.push-type.liveactivity
│ │ └─ Using plain bundle ID? → Append .push-type.liveactivity
│ └─ Topic correct → continue
│
├─ Push type header correct?
│ ├─ apns-push-type: liveactivity?
│ │ └─ Using "alert"? → Must be "liveactivity"
│ └─ Correct → continue
│
├─ Content-state matches ActivityAttributes.ContentState?
│ ├─ JSON keys match Swift property names exactly?
│ │ └─ Mismatch? → Decoding fails silently
│ └─ Using custom CodingKeys or JSONEncoder strategies?
│ └─ Custom strategies NOT supported — use default key encoding
│
├─ Push token being sent to server?
│ ├─ Observing pushTokenUpdates on the Activity?
│ │ └─ Missing? → Must iterate Activity.pushTokenUpdates async sequence
│ └─ Token changes when Activity restarts?
│ └─ Must handle token rotation — send updated token to server
│
└─ Rate limiting?
├─ Frequent updates: ~10-12 per hour per Activity
│ └─ Exceeding? → Batch updates, reduce frequency
└─ Alert updates (sound/vibration): ~3-4 per hour
└─ Exceeding? → Reserve alerts for critical state changes
树 6:iOS 更新后通知停止
Notifications stopped working after iOS update?
│
├─ Focus mode auto-enabled? (iOS 15+)
│ ├─ Check Settings > Focus
│ │ └─ Focus active? → App may not be in allowed list
│ └─ No Focus active → continue
│
├─ Interruption level filtering?
│ ├─ Default level is .active (may be filtered by Focus)
│ │ └─ Need to break through Focus? → Use .timeSensitive or .critical
│ ├─ .timeSensitive requires capability
│ │ └─ Missing? → Add Time Sensitive Notifications capability
│ └─ .critical requires Apple entitlement
│ └─ Only for health/safety/security apps — apply via Apple Developer
│
├─ Provisional authorization behavior changed?
│ ├─ iOS 15+ provisional notifications appear in Notification Summary
│ │ └─ User may not see them → Request full authorization
│ └─ Was relying on provisional? → Prompt for explicit permission
│
└─ Communication notifications require INSendMessageIntent?
├─ iOS 15+ communication notifications need SiriKit intent
│ └─ Missing? → Donate INSendMessageIntent before showing notification
└─ Intent donated but still filtered?
└─ Check that sender is in user's contacts
推送通知控制台工作流
Apple 的推送通知控制台支持无需服务器的测试:
前往控制台
- 打开 https://icloud.developer.apple.com/dashboard
- 在侧边栏中选择“Push Notifications”
- 选择你的应用的 Bundle ID
发送测试通知
- 输入设备令牌(来自步骤 2 的诊断)
- 选择环境(Sandbox/Production)
- 编写负载或使用模板
- 发送并查看送达状态
查看送达日志
- 从推送请求的响应头中复制
apns-id - 使用推送通知控制台按
apns-id查询送达状态 - 状态显示:已接受、已送达、已丢弃(附带原因)
常见的控制台查询结果
| 状态 | 含义 | 操作 |
|---|---|---|
| 已送达 | APNs 已送达设备 | 问题出在设备端(身份验证、专注模式、扩展) |
| 已丢弃:未注册 | 令牌无效 | 重新注册设备 |
| 已丢弃:设备令牌不属于此主题 | Bundle ID 不匹配 | 修正 apns-topic 请求头 |
| 已存储 | 设备离线,稍后将送达 | 等待或检查设备连接状态 |
使用 simctl 测试模拟器
模拟器无法注册远程通知,但你可以测试通知处理:
cat > test-push.apns << 'EOF'
{
"Simulator Target Bundle": "com.your.bundle.id",
"aps": {
"alert": {
"title": "Test",
"body": "Hello from simctl"
},
"sound": "default",
"mutable-content": 1
}
}
EOF
xcrun simctl push booted com.your.bundle.id test-push.apns
模拟器限制
- ✅ 通知外观和内容
- ✅ 通知服务扩展处理
- ✅ 通知内容扩展(自定义 UI)
- ✅ 操作处理和类别
- ❌ APNs 令牌注册(始终失败)
- ❌ 准确测试静默推送唤醒应用
- ❌ 实时活动推送更新
拖放替代方案
将 .apns 文件直接拖到模拟器窗口即可送达。负载中必须包含 "Simulator Target Bundle" 键。
常见 FCM 诊断
Swizzling 冲突
症状:使用 Firebase 时令牌回调未触发
原因:已禁用方法 Swizzling,但未实现手动转发
诊断:
// Check Info.plist
// FirebaseAppDelegateProxyEnabled = NO means YOU must forward tokens
修复(如果已禁用 Swizzling):
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Messaging.messaging().apnsToken = deviceToken
}
令牌不匹配
症状:服务器有 FCM 令牌,但 APNs 送达失败
原因:FCM 令牌和 APNs 令牌不同。FCM 封装了 APNs 令牌。
诊断:
// FCM token (send this to YOUR server)
Messaging.messaging().token { token, error in
print("FCM token: \(token ?? "nil")")
}
// APNs token (FCM handles this internally)
// Do NOT send raw APNs token to your server when using FCM
Firebase 控制台中缺少 APNs 密钥
症状:FCM 在 Android 上正常工作,但 iOS 上收不到通知
修复:
- Firebase 控制台 → 项目设置 → Cloud Messaging
- 上传 APNs 身份验证密钥(.p8)
- 输入 Key ID 和 Team ID
- 确认 Bundle ID 与应用一致
快速参考表
| 症状 | 检查项 | 修复方法 |
|---|---|---|
| 完全收不到通知 | 步骤 1:权利配置 | 启用 Push Notification 功能并重新生成描述文件 |
| 令牌注册失败 | 步骤 2:回调 | 在 AppDelegate 中实现这两个委托方法 |
| 400 BadDeviceToken | 令牌格式 | 重新注册;检查十六进制编码(无空格、无尖括号) |
| 403 InvalidProviderToken | JWT/证书 | 重新生成 JWT;验证 Key ID、Team ID、Bundle ID |
| 410 Unregistered | 设备状态 | 应用已卸载或令牌已轮换 — 从服务器移除 |
| 开发环境正常,生产环境异常 | 步骤 3:分别使用 curl 测试两者 | 切换 APNs 端点;不同环境的令牌不同 |
| 静默推送被忽略 | 负载 + 请求头 | content-available: 1,push-type: background,priority: 5 |
| 缺少富媒体 | 扩展 | 添加 mutable-content: 1,检查扩展 Bundle ID 和超时设置 |
| 实时活动信息过时 | 主题格式 | 使用 {bundleID}.push-type.liveactivity 主题 |
| 专注模式过滤通知 | 中断级别 | 对重要通知使用 .timeSensitive |
| FCM 在 iOS 上失败 | Firebase 控制台 | 上传 .p8 密钥,并填写正确的 Key ID 和 Team ID |
| 未显示操作 | 类别 ID | 确保负载中的类别标识符与已注册的类别一致 |
压力场景
场景 1:“服务端团队说问题出在 iOS 端”
背景:推送通知停止工作。后端团队表示他们的负载没有问题,问题肯定出在应用中。
压力:跳过客户端诊断,直接认定服务端没问题。开始重写通知处理代码。
实际情况:55% 的推送失败是权限配置或令牌问题(步骤 1-2),而不是代码错误。服务端可能发送到了错误的环境,或使用了过期令牌。
正确做法:先完成全部 4 个必需的诊断步骤,再修改代码。将 curl 测试(步骤 3)的结果分享给服务端团队——这能客观证明问题出在哪一端。
回应模板:“让我先核实客户端链路——我可以在 5 分钟内分享 curl 测试结果,这样我们双方都能准确知道故障出在哪。”
场景 2:“iOS 更新后通知停止工作,今天就要发布修复”
背景:用户反馈,更新到新的 iOS 版本后,通知停止工作。管理层要求今天就发布热修复。
压力:立即开始调试通知代码。认定是 Apple 弄坏了什么。
实际情况:新的 iOS 版本通常会默认启用专注模式,或更改中断级别过滤设置。15% 的推送失败是由专注模式或中断抑制造成的——你这边不需要修改代码。
正确做法:查看树 6(“iOS 更新后通知停止工作”)。修改代码前,先在测试设备上检查专注模式设置。如果专注模式正在过滤通知,修复方法是在负载中设置正确的 interruption-level,而不是重写通知处理逻辑。
回应模板:“iOS 更新经常会更改专注模式的默认设置。让我先检查中断级别——如果问题出在这里,只需修改一行负载,而不必重写代码。”
场景 3:“静默推送上周还能用,什么都没改”
背景:通过静默推送进行的后台内容同步停止工作了。“我们什么都没改。”
压力:深入检查后台执行代码。认定是回归问题。
实际情况:静默推送有系统强制执行的节流额度(每小时约 2-3 次)。如果使用量增加,或用户强制退出了应用,静默推送就会停止工作,与代码质量无关。此外,配置描述文件可能在重新生成时未包含推送权限。
正确做法:按照树 3(“静默通知未唤醒应用”)进行排查。调试代码前,先检查节流额度、强制退出状态和权限配置。
回应模板:“静默推送有系统节流额度。让我确认我们没有超出额度,并检查测试设备上的应用是否被强制退出——这是两个最常见的原因。”
检查清单
升级处理推送通知问题前:
- 在 Xcode 中启用 Push Notification 功能(步骤 1)
- 配置描述文件包含 aps-environment(步骤 1)
- 令牌注册回调已触发,并收到 64 位十六进制令牌(步骤 2)
- curl 请求 APNs 返回 HTTP/2 200(步骤 3)
- 用户已授权通知,状态 = 2(步骤 4)
- APNs 环境与构建类型匹配(沙盒/生产)
- 测试设备上的专注模式未过滤通知
- 已在实体设备上测试(令牌注册不能使用 Simulator)
- 对于 FCM:已在 Firebase Console 中上传 APNs 身份验证密钥
- 对于静默推送:已启用后台模式,优先级为 5,且不包含提醒键
资源
WWDC:2021-10091、2023-10025、2023-10185
文档:/usernotifications、/usernotifications/testing-notifications-using-the-push-notification-console
技能:axiom-push-notifications、axiom-push-notifications-ref、axiom-extensions-widgets