Notification Service Extension 是一个iOS扩展,它在推送通知显示给用户之前拦截它,并允许修改内容。该扩展可以解密加密数据、下载媒体附件、更改文本或添加自定义字段。根据 Apple Developer, 2025,Notification Service Extension 有最多30秒的时间在后台执行任务。
要点
Notification Service Extension 是iOS中的一个应用扩展,在推送通知显示之前启动。它允许服务器发送最小的payload,并由扩展用内容补充:下载图像、解密数据、替换文本。用户看到的是已经准备好的、处理过的通知。
当iOS收到推送通知时,系统会检查该应用程序是否存在扩展。如果扩展已注册,iOS会在后台进程中启动它,并通过 didReceive(_:withContentHandler:) 方法传递通知内容。扩展处理内容并使用修改后的内容调用 contentHandler。如果扩展未能在30秒内完成,系统将显示原始通知。
在通知内容需要在设备上处理的场景中,Service Extension 是必需的。安全通信:服务器发送加密的payload,扩展在本地解密。富媒体通知:服务器发送图像URL,扩展下载并附加它。动态本地化:扩展用设备语言替换文本。
| 场景 | 无扩展 | 有扩展 |
|---|---|---|
| 图像 | 不支持 | 下载并显示 |
| 加密 | 服务器保存密钥 | 在设备上解密 |
| 文本 | 在服务器上固定设置 | 动态替换 |
| 验证 | 不检查 | 取消恶意通知 |
Notification Service Extension 在受限环境中运行。最大执行时间为 30秒。扩展无法访问应用程序的主存储(App Group 除外)。扩展的大小限制为50 MB。超时时,系统使用原始内容调用 contentHandler,所有更改都将丢失。
在 Xcode 中,通过创建 Notification Service Extension 类型的新目标来添加 Notification Service Extension。Xcode 会生成一个继承自 UNNotificationServiceExtension 的 NotificationService 类模板,包含两个方法:didReceive 和 serviceExtensionTimeWillExpire。
在 Xcode 中选择 File → New → Target → Notification Service Extension。指定名称(例如 PushNotificationService)和 Swift 语言。确保目标已添加到主应用程序并且签名正确。创建后将生成 NotificationService.swift 类及其基本实现。
扩展的 Info.plist 包含 NSExtension 键,其子键为 NSExtensionPointIdentifier (com.apple.usernotifications.service) 和 NSExtensionPrincipalClass(控制器的名称)。可选地,可以指定 NSExtensionAttributes 以及扩展的激活规则。这些设置由 Xcode 自动生成。
基本实现重写 didReceive,修改通知内容并调用 contentHandler。如果处理时间过长,将调用 serviceExtensionTimeWillExpire,此时需要以当前状态完成工作。
import UserNotifications
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(
request: UNNotificationRequest,
withContentHandler handler: @escaping (UNNotificationContent) -> Void
) {
contentHandler = handler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent {
bestAttemptContent.title = "[已处理] \(bestAttemptContent.title)"
contentHandler?(bestAttemptContent)
}
}
override func serviceExtensionTimeWillExpire() {
if let contentHandler = contentHandler,
let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
Service Extension 的关键应用之一是在设备上 解密 通知内容。服务器发送加密的payload,扩展使用存储在 Keychain 或 App Group 中的密钥进行解密。这保证了内容在传输过程中无法被拦截。
服务器使用对称密钥 (AES-256) 加密通知正文。加密密钥在客户端和服务器之间预先协商。扩展在 payload 的数据字段中接收加密字符串,将其解密并放置到通知字段中。用户密钥 可以在身份验证后从 Keychain 获取。
override func didReceive(
request: UNNotificationRequest,
withContentHandler handler: @escaping (UNNotificationContent) -> Void
) {
guard let content = request.content.mutableCopy()
as? UNMutableNotificationContent else {
handler(request.content)
return
}
guard let encryptedData = content.userInfo["encrypted_data"]
as? String else {
handler(content)
return
}
let decrypted = CryptoService.decrypt(encryptedData)
content.body = decrypted.body
content.title = decrypted.title
handler(content)
}
解密密钥不应存储在扩展代码或 NSUserDefaults 中。使用 iOS Keychain 并通过 App Group 访问,以便应用程序和扩展都可以读取密钥。要在客户端生成密钥,请使用带有 AES-256-GCM 算法的 Security.framework。
Notification Service Extension 最常见的用途是下载图像、GIF 和视频以便在通知中显示。服务器发送媒体文件的 URL,扩展下载它,将其保存到临时目录并创建 UNNotificationAttachment。
扩展从通知的 payload 字段接收图像 URL。使用 URLSession,扩展将文件下载到临时目录。下载完成后,使用本地 URL 创建 UNNotificationAttachment。附件被传递给修改后的内容。iOS 自动 在标准界面或 Notification Content Extension 中显示图像。
private func downloadAndAttachMedia(
content: UNMutableNotificationContent,
mediaUrl: String,
handler: @escaping (UNNotificationContent) -> Void
) {
guard let url = URL(string: mediaUrl) else {
handler(content)
return
}
let task = URLSession.shared.downloadTask(with: url) { localUrl, _, error in
guard let localUrl = localUrl, error == nil else {
handler(content)
return
}
let attachment = try? UNNotificationAttachment(
identifier: "media",
url: localUrl,
options: nil
)
if let attachment = attachment {
content.attachments = [attachment]
}
handler(content)
}
task.resume()
}
iOS 支持以下格式在通知中显示:JPEG、PNG、GIF(静态)、MPEG-4 视频(最大50 MB)。对于音频文件,支持 MP3、AAC 和 ALAC。重要:所有媒体文件必须在30秒限制内下载。对于大文件,建议使用服务器端裁剪或渐进式下载。
管理30秒限制 是开发 Notification Service Extension 的关键任务。如果扩展未能完成处理,系统将调用 serviceExtensionTimeWillExpire 并显示原始内容。必须为每种处理类型预设备用方案。
按优先级划分任务。关键修改(解密、基本本地化)首先执行。可选改进(图像下载、文本丰富化)其次执行。对于网络请求,使用 带超时的 URLSession,以避免将整个限制花费在单个操作上。
如果图像下载失败或 payload 解密返回错误,扩展应使用原始内容调用 contentHandler。切勿在不调用 contentHandler 的情况下结束扩展——这会导致通知丢失。安全备用 应始终在扩展代码中预设。
override func didReceive(
request: UNNotificationRequest,
withContentHandler handler: @escaping (UNNotificationContent) -> Void
) {
let content = (request.content.mutableCopy()
as? UNMutableNotificationContent) ?? request.content
// 关键任务:解密
var decryptedContent = tryDecryptPayload(content)
// 可选任务:媒体
guard let mediaUrl = decryptedContent.userInfo["media_url"]
as? String else {
handler(decryptedContent)
return
}
downloadAndAttachMedia(
content: decryptedContent,
mediaUrl: mediaUrl,
handler: handler
)
}
要测试 Notification Service Extension,请使用 Xcode:选择扩展目标,在模拟器上运行,并通过终端或 Firebase Console 发送推送通知。通过 os_log 记录 每个处理阶段——这有助于诊断定时和加载错误问题。
常见问题
系统将调用 serviceExtensionTimeWillExpire,然后显示原始通知而不做任何更改。所有下载的文件和修改都将被丢弃。
是的,如果使用空内容(具有空字段的 UNNotificationContent)调用 contentHandler,通知将不会显示。这用于过滤 垃圾通知 或不正确的数据。
令牌可以在通知 payload 的 userInfo 中传递,或通过 App Group 从 Keychain 获取。不建议将令牌存储在扩展的 UserDefaults 中。
扩展必须使用与主应用程序相同的开发者证书签名。对于生产环境,需要 生产证书 并启用 Push Notifications 功能。
将设备连接到 Xcode,在启动方案中选择扩展目标,并通过 Firebase Console 发送推送通知。扩展中的 断点 与主应用程序中的工作方式相同。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。