The Push Notifications primer
System Frameworks 入门 30m

推送通知入门指南

The Push Notifications primer

2020年6月25日

在 Apple 官方观看视频

一句话判断

如果你对 APNs 的完整链路(Device Token -> Provider Server -> APNs -> Device)还似懂非懂,这个 Session 用一个多小时的完整讲解帮你从头梳理清楚推送通知的每个环节。

这场 Session 讲了什么

这是一个面向推送通知初学者的完整教程。Session 从最基础的概念开始讲解:什么是远程推送、为什么需要 Device Token、APNs 的工作原理、以及如何在客户端和服务端分别实现推送功能。

Session 把推送链路拆解为四个步骤。第一步:设备向 APNs 注册,获取 Device Token。第二步:将 Device Token 发送到你自己的 Provider Server。第三步:Provider Server 用 Token 向 APNs 发送推送请求。第四步:APNs 将推送投递到用户设备。每一步都配有详细的代码示例和常见错误说明。Session 还介绍了 iOS 14 中推送通知的新变化,包括改进的后台推送处理和更丰富的通知内容扩展。

值得深挖的点

Device Token 的生命周期管理

Device Token 不是一成不变的。系统会在某些情况下更新 Token(比如系统升级、备份恢复、用户在多设备间切换)。你的 App 必须在每次启动时都调用注册接口,并在 Token 变化时将新 Token 上报给服务器。Apple 还特别强调:不要缓存 Token 然后跳过注册,也不要尝试在不同设备间共享 Token。Token 是和特定设备绑定的。

后台推送 vs 可见推送

推送分为两种类型:静默推送(content-available: 1)和可见推送(带 alert/body)。静默推送不会显示任何 UI,只是唤醒 App 让它在后台处理数据。可见推送会显示系统通知。关键区别在于:静默推送受系统节流(throttling)限制,如果用户很少打开你的 App,系统会降低静默推送的投递频率。可见推送则不受这个限制。

代码片段

注册远程推送并处理 Device Token

场景:在 App 启动时注册推送并获取 Device Token。

import UIKit
import UserNotifications

class AppDelegate: UIResponder, UIApplicationDelegate {
    
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        // 第一步:请求用户授权
        UNUserNotificationCenter.current().requestAuthorization(options: [
            .alert, .badge, .sound
        ]) { granted, error in
            if granted {
                print("用户授权了通知权限")
                // 第二步:注册远程推送
                DispatchQueue.main.async {
                    application.registerForRemoteNotifications()
                }
            } else {
                print("用户拒绝了通知权限")
            }
        }
        
        return true
    }
    
    // 第三步:处理注册成功,获取 Device Token
    func application(_ application: UIApplication,
                     didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        // 将 Data 转换为十六进制字符串
        let tokenString = deviceToken.map { String(format: "%02x", $0) }.joined()
        print("Device Token: \(tokenString)")
        
        // 发送到你的 Provider Server
        sendTokenToServer(tokenString)
    }
    
    // 处理注册失败
    func application(_ application: UIApplication,
                     didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("注册推送失败: \(error)")
        // 可能是模拟器、无网络、或 APNs 不可用
    }
    
    private func sendTokenToServer(_ token: String) {
        // 将 Token 发送到你的后端服务器
        // 使用 HTTPS 请求,不要在 URL 中传递 Token
        guard let url = URL(string: "https://api.yourapp.com/register-token") else { return }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body = ["device_token": token, "platform": "ios"]
        request.httpBody = try? JSONSerialization.data(withJSONObject: body)
        
        URLSession.shared.dataTask(with: request).resume()
    }
}

处理收到的推送通知

场景:处理前台和后台的推送消息。

// 设置通知代理
class AppDelegate: NSObject, UNUserNotificationCenterDelegate {
    
    func setupNotificationCenter() {
        UNUserNotificationCenter.current().delegate = self
    }
    
    // 前台收到推送时的处理
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        
        let userInfo = notification.request.content.userInfo
        
        // 检查是否是静默推送
        if let aps = userInfo["aps"] as? [String: Any],
           aps["content-available"] as? Int == 1 {
            // 静默推送:在后台处理数据
            handleBackgroundUpdate(userInfo)
            completionHandler([])  // 不显示通知 UI
        } else {
            // 可见推送:显示通知横幅
            completionHandler([.banner, .sound, .badge])
        }
    }
    
    // 用户点击通知后的处理
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {
        
        let userInfo = response.notification.request.content.userInfo
        
        // 根据通知内容导航到对应页面
        if let articleId = userInfo["article_id"] as? String {
            navigateToArticle(id: articleId)
        }
        
        completionHandler()
    }
    
    private func handleBackgroundUpdate(_ userInfo: [AnyHashable: Any]) {
        // 下载新内容、更新数据库等
        print("处理后台更新: \(userInfo)")
    }
}

Provider Server 端发送推送(伪代码参考)

场景:你的后端服务器如何向 APNs 发送推送请求。

// 这段代码展示了 Provider Server 端的逻辑
// 实际使用时通常用 Node.js/Python/Go 等后端语言

// APNs 请求的 HTTP/2 格式
// POST https://api.push.apple.com/3/device/{device_token}
// Headers:
//   authorization: bearer {jwt_token}
//   apns-topic: com.yourapp.bundleid
//   apns-push-type: alert  (或 background)
// Body:
// {
//   "aps": {
//     "alert": {
//       "title": "新消息",
//       "body": "你收到了一条新消息"
//     },
//     "badge": 1,
//     "sound": "default"
//   },
//   "article_id": "12345"  // 自定义数据
// }

// 静默推送格式
// {
//   "aps": {
//     "content-available": 1
//   },
//   "update-type": "new-content"
// }

最佳实践

已有项目:检查你的 Token 注册逻辑是否在每次 App 启动时都执行。如果只在首次安装时注册,你可能丢失了 Token 更新的情况。确保你的通知代理同时处理了前台和后台的场景。对于静默推送,不要在回调中做耗时过长的操作——系统给了你大约 30 秒的处理时间。

新项目:从第一天就集成推送通知,不要等到产品上线后再加。在开发阶段使用 APNs 的沙箱环境(sandbox.push.apple.com),发布时切换到生产环境。建议封装一个推送服务管理类,统一处理注册、Token 管理、消息接收等逻辑。

还有什么值得关注

  • iOS 14 要求在推送请求的 Header 中包含 apns-push-type 字段,否则推送可能被延迟。
  • Token 的沙箱版本和生产版本是不同的,测试推送和生产推送要使用对应的 Token。
  • 推送通知的速率限制(rate limiting):APNs 对单个设备的推送频率有隐式限制,避免短时间内发送大量推送。
WWDC 2020