什么是 APNs?
APNs 是苹果提供的云端推送服务,它的核心作用是:允许第三方应用将通知“推”到用户的 iOS 设备上,即使应用本身没有在运行。

没有 APNs 的话,如果应用想通知用户新消息,必须自己保持一个后台长连接,这会非常耗电和耗流量,APNs 将这个任务集中到了苹果的服务器上,极大地优化了 iOS 的后台通知机制。
APNs 的工作流程
一个完整的推送流程如下,理解这个流程是关键:
(这是一个简化的流程图,下面是文字详解)
-
开发者服务器 (Provider)
(图片来源网络,侵删)这是你的应用后端服务器,当有需要通知用户的事件发生时(用户 A 给用户 B 发了一条新消息),你的服务器会准备一条推送消息。
-
向 APNs 发送推送请求
- 你的服务器会使用一个安全的网络连接,将这条推送消息发送到 APNs 服务器。
- 为了让 APNs 知道要把消息推送给哪个设备,你的服务器必须在消息中包含一个唯一的 设备令牌。
-
APNs 处理和路由
- APNs 接收到请求后,会根据设备令牌查找对应的 iOS 设备。
- APNs 会检查该设备是否:
- 在线且开启了推送权限。
- 安装了对应的应用。
- 没有被用户禁用该应用的推送通知。
- 如果一切正常,APNs 就会将消息通过安全的通道发送到目标设备。
-
iOS 设备接收通知
(图片来源网络,侵删)- iOS 设备的系统层接收到来自 APNs 的消息。
- 系统会根据应用当前的运行状态和通知的配置,进行不同的处理:
- 应用在前台:系统会将通知数据直接传递给正在运行的应用程序,由应用自行处理(在界面上显示一个徽章角标或更新 UI),用户不会看到系统级的通知弹窗。
- 应用在后台或未运行:系统会显示一个系统级的通知弹窗、在应用图标上显示徽章角标,或者播放提示音,用户点击通知后,系统会唤醒你的应用。
核心组件详解
设备令牌
- 是什么? 一个由 APNs 生成的、与特定设备和特定应用绑定的唯一标识符,它相当于你的应用在 APNs 上的“手机号码”。
- 如何获取?
- 当你的 App 首次启动并请求推送权限时,iOS 系统会向 APNs 申请一个令牌。
- APNs 生成这个令牌,并通过 iOS 系统返回给你的 App。
- 你的 App 需要将这个令牌立即、安全地发送到你的开发者服务器。
- 你的服务器必须妥善存储这个令牌,以便后续发送推送。
- 为什么重要? 这是推送的“地址”,没有正确的设备令牌,APNs 就不知道把消息发给谁。
- 令牌失效: 如果用户卸载应用、重置系统、或者更新系统后,旧的令牌会失效,你的应用需要监听
didRegisterForRemoteNotificationsWithDeviceToken和didFailToRegisterForRemoteNotificationsWithError这两个代理方法,在令牌变化时及时更新到你的服务器。
SSL 证书 / .p8 私钥
为了与 APNs 通信,你的开发者服务器必须进行身份验证,证明它是“合法”的,苹果提供了两种主流的认证方式:
p12 证书(传统方式)
- 创建证书:在 Apple Developer Portal 中,为你的 App ID 创建一个 "Apple Push Notification service SSL (Sandbox & Production)" 证书。
- 下载证书:下载这个
.cer文件。 - 生成私钥:在你的 Mac 上使用 Keychain Access 生成一个私钥,并将其与
.cer文件导出为一个.p12文件,这个.p12文件包含了你的公钥和私钥。 - 使用:你的服务器需要这个
.p12文件及其密码来建立与 APNs 的 SSL 连接。
.p8 私钥(推荐方式,更简单)
- 创建私钥:在 Apple Developer Portal 中,生成一个 "Auth Key (ID:
YOUR_KEY_ID)",下载这个.p8文件。注意:.p8 文件只能下载一次,请务必保管好! - 所需信息:
.p8文件内容。- Key ID:在 Portal 中可以找到。
- Team ID:你的开发者团队 ID。
- Bundle ID:你的应用 Bundle ID。
- 使用:这种方式更现代,无需密码管理,只需在代码中提供
.p8文件和 Key ID 等信息即可生成 JWT (JSON Web Token) 用于认证。
重要提示:
- 开发环境 vs. 生产环境:APNs 分为 Sandbox (开发) 和 Production (生产) 两个环境,你的
.p12或.p8文件也对应这两种环境,开发时用 Sandbox,上线后必须用 Production,否则推送不到真机。 - VoIP 推送:如果做语音通话类应用,需要申请 VoIP 证书,流程类似,但使用的是
pushkit框架。
如何在代码中实现(iOS 端)
请求推送权限
在 AppDelegate 或 SceneDelegate 中,当应用启动时,向用户请求发送通知的权限。
import UIKit
import UserNotifications
// iOS 10 及以上
func requestNotificationAuthorization() {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if let error = error {
print("请求权限失败: \(error.localizedDescription)")
}
print("用户授权结果: \(granted)")
// 如果用户授权,可以在这里注册获取 Device Token
if granted {
registerForRemoteNotifications()
}
}
}
// 注册获取 Device Token (iOS 3 及以上)
func registerForRemoteNotifications() {
UIApplication.shared.registerForRemoteNotifications()
}
获取并处理 Device Token
// AppDelegate.swift
// 注册成功
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// 将 Data 类型的 deviceToken 转换为 String
let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
print("获取到 Device Token: \(tokenString)")
// 将这个 tokenString 发送到你的服务器进行存储
// sendTokenToServer(token: tokenString)
}
// 注册失败
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("获取 Device Token 失败: \(error.localizedDescription)")
}
处理收到的通知
根据应用状态,处理接收到的通知。
// 在 SceneDelegate (iOS 13+) 或 AppDelegate 中
// 当应用在前台时收到通知
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
// 即使在前台,我们也想显示一个弹窗和播放声音
completionHandler([.banner, .sound, .badge])
}
// 当用户点击通知时触发
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
// 从 userInfo 中解析数据,并跳转到对应界面
// handleDeepLink(userInfo: userInfo)
print("用户点击了通知, userInfo: \(userInfo)")
completionHandler()
}
如何在代码中实现(服务器端)
服务器端的实现取决于你使用的编程语言(Java, Python, Node.js, PHP 等),这里以 Node.js 为例,使用 apn 库。
-
安装库:
npm install apn
-
编写推送代码:
const apn = require('apn');
// 1. 创建 APNs 提供者实例
// 注意:请将 YOUR_AUTH_KEY.p8 文件放在项目根目录,并填入你的 Key ID 和 Team ID
const options = {
token: {
key: 'YOUR_AUTH_KEY.p8', // .p8 文件的路径或内容
keyId: 'YOUR_KEY_ID',
teamId: 'YOUR_TEAM_ID',
},
production: false // 设置为 true 则连接生产环境
};
const apnProvider = new apn.Provider(options);
// 2. 准备推送通知
const note = new apn.Notification({
alert: "你好,这是一条来自服务器的推送!",
sound: "ping.aiff",
badge: 1,
payload: {
"customData": { "type": "message", "id": 123 }
}
});
// 3. 设备令牌 (从你的数据库中获取)
const deviceToken = 'YOUR_DEVICE_TOKEN_HERE'; // 注意:这个 token 是字符串,不是 Data
// 4. 发送推送
apnProvider.send(note, deviceToken).then((result) => {
console.log("推送结果:", result);
}).catch((error) => {
console.error("推送失败:", error);
});
// 5. 关闭连接 (非常重要,否则会一直占用资源)
apnProvider.shutdown();
常见问题与最佳实践
-
推送失败,设备令牌无效
- 原因:用户卸载了应用、更新了 iOS 系统、或者在设置里关闭了该应用的推送权限。
- 解决:你的服务器需要实现一个反馈服务机制,APNs 有一个专门的反馈服务,它会定期推送一个无效的设备令牌列表给你的服务器,你的服务器需要定期检查这个列表,并从数据库中删除这些无效的令牌,避免无效推送。
-
为什么开发机能收到,真机收不到?
- 证书问题:检查你的服务器是否使用了 Sandbox 环境的证书/私钥。
- Bundle ID 不匹配:你的 App ID 和 Provisioning Profile 中的 Bundle ID 必须与你的应用完全一致。
- 权限问题:确保在真机上运行时,弹出了推送权限请求,并且用户点击了“允许”。
- 后台模式:对于后台下载、静默推送等,需要在 Xcode 的 "Signing & Capabilities" 中开启 "Background Modes" 对应的能力。
-
推送到达率
- 频率限制:APNs 对推送频率有限制,不要过于频繁地推送。
- 内容质量:避免发送垃圾信息,否则用户会直接禁用你的推送。
- VoIP 推送:对于即时性要求高的应用(如聊天、电话),强烈建议使用 VoIP 推送,它的优先级更高,到达速度更快。
-
限制
- APNs 对通知的大小和内容有严格限制(
alert字符串长度有限制)。 - 对于大量数据,应该只发送一个标识符(如
message_id),让 App 收到通知后,再通过网络请求从你的服务器拉取完整数据。
- APNs 对通知的大小和内容有严格限制(
iOS 推送是一个涉及客户端、服务器端和苹果三方服务的完整系统,核心在于:
- 客户端:正确请求权限、获取 Device Token 并上报给服务器。
- 服务器端:安全地认证 APNs、使用有效的 Device Token 发送格式正确的推送消息。
- 证书管理:分清开发/生产环境,妥善保管
.p12或.p8文件。
理解了 APNs 的工作原理和流程,你就能顺利地在你的应用中集成推送功能了。
