文章

iOS 推送通知:从 APNs 到前台展示的全链路深度解析

全面解析 iOS 远程推送通知的完整流程,从 APNs 通信机制、推送证书与 Key 管理、UNUserNotificationCenter 注册与权限请求,到前台展示、静默推送和数据解析,并涵盖 Flutter/RN 中的推送集成实践。

iOS 推送通知:从 APNs 到前台展示的全链路深度解析

一句话概括

iOS 推送通知是一个涉及 Provider 服务器、Apple Push Notification Service(APNs)和客户端三大角色协同工作的消息传递体系,通过严格的证书/Token 认证机制确保通知只到达目标设备;UNUserNotificationCenter 负责客户端侧的注册、权限请求和通知展示,而静默推送(content-available)则提供了后台唤醒应用执行代码的能力。

背景与意义

推送通知是移动应用不可或缺的基础能力。无论是即时通讯的消息提醒、电商应用的订单状态更新、社交媒体的点赞通知,还是新闻应用的突发新闻推送,推送通知都是保持用户参与度的关键手段。

对于跨平台开发者而言,推送通知的集成一直是痛点之一。在 Flutter 生态中,firebase_messaging 插件提供了统一的推送解决方案;在 React Native 中,@react-native-firebase/messagingreact-native-push-notification 被广泛使用。这些跨平台插件封装了 iOS 和 Android 的推送差异,但当推送出现问题时——用户收不到通知、通知点击后没有跳转到正确页面、后台推送不触发——你就必须深入底层理解 iOS 推送机制来定位问题。

iOS 推送通知有三大特殊性,使其与其他平台有显著差异:

  1. 严格的证书/Key 管理:每次推送都需要验证身份,不像 Android 的 FCM 只需要 Server Key
  2. 明确的权限请求机制:用户在首次打开应用时会收到权限弹窗,可以选择拒绝或以后更改
  3. 复杂的通知分类:远程推送(Remote Notification)、本地推送(Local Notification)、静默推送(Silent Push)三者的处理逻辑完全不同

本文将深入剖析 iOS 推送通知的完整技术栈,从 APNs 协议层面到客户端集成层面,帮助跨平台开发者建立全面的推送知识体系。

核心知识点拆解

一、远程推送的全链路流程

iOS 远程推送通知的完整路径涉及三个核心角色:

1
Provider(你的服务器) → APNs(Apple 推送服务) → Device(用户设备)

1. 设备注册与 Token 获取

当用户首次打开应用时,iOS 会向 APNs 发起设备注册请求。APNs 返回一个 device token——这是一个二进制数据的哈希值,唯一标识了当前设备上的当前应用。应用需要将这个 token 发送到自己的 Provider 服务器保存。

获取 token 的代码流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import UIKit
import UserNotifications

// 在 AppDelegate 中注册推送通知
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // 注册远程通知(会触发向 APNs 请求 token)
    UIApplication.shared.registerForRemoteNotifications()
    return true
}

// 成功获取 token 的回调
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
    let token = tokenParts.joined()
    print("Device Token: \(token)")
    // 将 token 发送到你的服务器
    sendDeviceTokenToServer(token)
}

// 获取 token 失败的回调
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
    print("Failed to register: \(error.localizedDescription)")
}

Token 的格式是一个十六进制字符串,例如:0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2

2. Provider 发送推送请求

当你的服务器需要向设备发送推送时,它需要:

  1. 使用 Apple 提供的认证方式(证书或 Key)建立与 APNs 的连接
  2. 构造一个 JSON 格式的推送 payload
  3. 通过 HTTP/2 协议发送 POST 请求到 APNs 服务器

APNs 服务器的地址:

  • 生产环境:api.push.apple.com:443
  • 开发环境:api.sandbox.push.apple.com:443

推送请求的 HTTP Header 包含:

1
2
3
4
5
6
7
:method = POST
:path = /3/device/{device_token}
authorization = bearer {provider_token}  // 使用 Token Authentication 时
apns-topic = com.yourcompany.yourapp    // Bundle Identifier
apns-priority = 10                       // 10=立即发送, 5=考虑省电发送
apns-expiration = 0                      // 过期时间(0=立即丢弃)
apns-push-type = alert                   // alert, background, voip, etc.

3. 推送 Payload 结构

APNs 推送的 payload 是一个 JSON 对象,最大 4KB(对于 alert 类型):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
    "aps": {
        "alert": {
            "title": "新消息",
            "subtitle": "张三",
            "body": "嗨,晚上一起吃饭吗?"
        },
        "sound": "default",
        "badge": 5,
        "category": "message",
        "thread-id": "thread_123",
        "mutable-content": 1
    },
    "custom_data": {
        "message_id": "msg_abc123",
        "sender_id": "user_456",
        "type": "text"
    }
}

aps 字典中的关键字段:

  • alert: 展示给用户的通知内容(可以是字符串或字典)
  • sound: 通知声音(”default” 或自定义声音文件名)
  • badge: 应用图标角标数字
  • category: 通知分类标识,用于展示交互式按钮
  • thread-id: 通知分组标识(iOS 12+)
  • mutable-content: 设为 1 表示允许 Notification Service Extension 修改通知内容
  • content-available: 设为 1 表示静默推送(后台唤醒应用)

4. APNs 响应

APNs 返回的 HTTP 状态码:

  • 200: 推送成功
  • 400: payload 格式错误
  • 403: 认证错误(证书无效或 token 过期)
  • 404: device token 无效(设备不再使用此 token)
  • 410: device token 已过期(需要从服务器删除此 token)
  • 413: payload 过大(超过 4KB)
  • 429: 请求过于频繁
  • 500: APNs 内部错误

收到 404 或 410 状态码时,服务器应该立即删除对应的 device token,避免继续向无效设备推送。

二、推送证书 vs 推送 Key(Token Authentication)

Apple 提供两种认证方式:

1. 推送证书(Certificate)

传统方式,每个 App 需要单独的推送证书。证书通过 Apple Developer Center 生成,包含在 .p12 文件中。服务器需要安装这个证书才能与 APNs 通信。

优点

  • 兼容性好,所有推送库都支持
  • 易于理解

缺点

  • 证书有效期一年,需要定期续期
  • 每个环境(开发/生产)需要单独的证书
  • 证书更换时需要重新配置服务器
  • 不支持多服务器同时推送(有并发限制)

2. 推送 Key(Token Authentication)

Apple 在 iOS 10+ 引入了基于 Token 的认证方式。你创建一个 .p8 私钥文件,用于生成 JWT(JSON Web Token)来认证每次推送请求。

优点

  • 推送 Key 永不过期(除非手动撤销)
  • 一个 Key 可用于所有应用(同一个 Team ID 下的所有 App)
  • 支持多服务器同时推送(无并发限制)
  • 服务器配置更简单

缺点

  • 需要服务器支持 JWT 生成
  • 部分老旧推送库可能不支持

Token Authentication 的认证流程

  1. 从 Apple Developer Center 下载 .p8 私钥文件,包含 EC 私钥和 Key ID
  2. 服务器使用私钥和 Key ID 生成 JWT Token:
1
2
3
4
{
    "iss": "YOUR_TEAM_ID",
    "iat": 1678900000
}
  1. JWT Header 中指定 Key ID 和算法:
1
2
3
4
{
    "alg": "ES256",
    "kid": "YOUR_KEY_ID"
}
  1. 使用 ES256(ECDSA with P-256)算法签名
  2. 生成的 JWT 有效期为 30 分钟,需要定期刷新

推荐使用 Token Authentication:除非你的推送库不兼容,否则推送 Key 是所有新项目的最佳选择。

三、UNUserNotificationCenter 注册与权限请求

iOS 10+ 使用 UNUserNotificationCenter 统一管理本地通知和远程通知。

1. 注册流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import UserNotifications

// 在 AppDelegate 中配置
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // 设置 UNUserNotificationCenter 的代理
    UNUserNotificationCenter.current().delegate = self
    
    // 请求通知权限
    let options: UNAuthorizationOptions = [.alert, .sound, .badge]
    UNUserNotificationCenter.current().requestAuthorization(options: options) { granted, error in
        if granted {
            print("通知权限已获取")
            DispatchQueue.main.async {
                application.registerForRemoteNotifications()
            }
        } else {
            print("用户拒绝了通知权限")
        }
    }
    
    return true
}

2. 权限请求的时机

  • iOS 会在第一次调用 requestAuthorization 时弹出权限弹窗
  • 如果用户拒绝,系统不会再自动弹窗(需要引导用户手动去 Settings 中开启)
  • 最佳实践:在用户真正需要推送功能时才请求权限(而不是应用启动时就请求)
  • 可以通过 getNotificationSettings 检查当前授权状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
UNUserNotificationCenter.current().getNotificationSettings { settings in
    switch settings.authorizationStatus {
    case .notDetermined:
        // 尚未请求权限
        // 在合适的时机请求权限
    case .denied:
        // 用户拒绝了权限
        // 引导用户前往设置页面
        DispatchQueue.main.async {
            if let url = URL(string: UIApplication.openSettingsURLString) {
                UIApplication.shared.open(url)
            }
        }
    case .authorized, .provisional, .ephemeral:
        // 已有权限
        break
    @unknown default:
        break
    }
}

3. 处理前台收到的通知

当应用在前台运行时收到推送通知,系统默认不会展示通知横幅。你需要实现 UNUserNotificationCenterDelegate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 应用在前台时收到推送通知
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    // iOS 14+ 使用 .banner 和 .list
    if #available(iOS 14.0, *) {
        completionHandler([.banner, .sound, .badge, .list])
    } else {
        completionHandler([.alert, .sound, .badge])
    }
}

// 用户点击或滑动通知时的处理
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
    let userInfo = response.notification.request.content.userInfo
    // 解析通知数据,跳转到对应页面
    handleNotificationTap(userInfo: userInfo)
    completionHandler()
}

4. 通知分组与分类

iOS 12+ 支持按 thread-id 对通知进行分组:

1
2
3
4
5
6
7
8
9
10
11
12
// 设置通知的 thread-id
UNUserNotificationCenter.current().setNotificationCategories([
    UNNotificationCategory(
        identifier: "message",
        actions: [
            UNNotificationAction(identifier: "reply", title: "回复", options: .authenticationRequired),
            UNNotificationAction(identifier: "mark_read", title: "标记已读", options: .authenticationRequired)
        ],
        intentIdentifiers: [],
        options: .customDismissAction
    )
])

四、前台展示与后台静默推送

1. 静默推送(Silent Push / content-available)

静默推送是一种特殊的远程通知,不会向用户展示任何内容,但可以在后台唤醒应用执行代码。它在 payload 中包含 "content-available": 1

1
2
3
4
5
6
7
8
{
    "aps": {
        "content-available": 1
    },
    "custom_data": {
        "type": "refresh_feeds"
    }
}

在应用端处理静默推送:

1
2
3
4
5
6
7
8
9
10
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    // 处理静默推送数据
    if let type = userInfo["type"] as? String, type == "refresh_feeds" {
        refreshFeeds { result in
            completionHandler(result)
        }
    } else {
        completionHandler(.noData)
    }
}

静默推送的关键限制

  • 不保证送达(iOS 可能根据设备状态推迟或丢弃)
  • 应用在后台时只有约 30 秒的执行时间
  • 设备处于低电量模式时可能被禁用
  • 用户可以通过 Settings 禁用应用的后台刷新能力
  • 推送频率限制:iOS 会限制静默推送的速率,过于频繁会被系统忽略

对于跨平台开发者来说,Flutter 的 firebase_messaging 插件通过 onBackgroundMessage 处理后台消息,原理就是利用了 iOS 的静默推送机制。

2. 携带 badge 数量

badge 是 iOS 应用图标右上角的红色数字。需要在服务器端和应用端都进行处理:

服务器推送 payload

1
2
3
4
5
{
    "aps": {
        "badge": 5
    }
}

应用端更新 badge

1
2
3
4
5
// 设置 badge 数量
UIApplication.shared.applicationIconBadgeNumber = 5

// 清除 badge
UIApplication.shared.applicationIconBadgeNumber = 0

五、推送通知中的 userInfo 与数据解析

1. 从不同入口获取推送数据

推送通知可以通过三种入口进入应用:

入口 A:应用处于前台,收到推送 回调:userNotificationCenter(_:willPresent:withCompletionHandler:)

入口 B:用户点击推送通知 回调:userNotificationCenter(_:didReceive:withCompletionHandler:)

入口 C:应用通过推送通知启动 回调:application(_:didFinishLaunchingWithOptions:) 中的 launchOptions

1
2
3
4
5
6
7
8
// 处理通过推送通知启动的情况
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    if let remoteNotif = launchOptions?[.remoteNotification] as? [String: AnyObject] {
        // 应用是通过点击推送通知启动的
        handleNotificationLaunch(userInfo: remoteNotif)
    }
    return true
}

2. 数据解析示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func parseNotificationPayload(_ userInfo: [AnyHashable: Any]) {
    guard let aps = userInfo["aps"] as? [String: Any] else { return }
    
    // 解析 alert
    if let alert = aps["alert"] as? [String: String] {
        let title = alert["title"]
        let body = alert["body"]
    } else if let alertString = aps["alert"] as? String {
        // alert 可以是纯字符串
        print("Alert: \(alertString)")
    }
    
    // 解析自定义数据
    let messageId = userInfo["message_id"] as? String
    let type = userInfo["type"] as? String
    let senderId = userInfo["sender_id"] as? String
    
    // 根据消息类型处理跳转
    switch type {
    case "text":
        navigateToConversation(senderId: senderId!)
    case "friend_request":
        navigateToFriendRequest(senderId: senderId!)
    default:
        break
    }
}

六、Notification Service Extension(通知服务扩展)

Notification Service Extension 是 iOS 10 引入的扩展,允许在推送通知展示给用户之前,修改或丰富通知内容。例如:

  • 解密端到端加密的消息内容
  • 下载并展示媒体附件(图片、视频、音频)
  • 修改通知的标题或正文

创建 Service Extension 后,需要在推送 payload 中设置 "mutable-content": 1

1
2
3
4
5
6
{
    "aps": {
        "alert": { "title": "新消息", "body": "[加密内容]" },
        "mutable-content": 1
    }
}

在 Service Extension 中解密:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class NotificationService: UNNotificationServiceExtension {
    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        let userInfo = request.content.userInfo
        guard let encryptedBody = userInfo["encrypted_body"] as? String else {
            contentHandler(request.content)
            return
        }
        
        // 解密消息
        let decryptedBody = decryptMessage(encryptedBody)
        
        // 修改通知内容
        let modifiedContent = request.content.mutableCopy() as! UNMutableNotificationContent
        modifiedContent.body = decryptedBody
        
        // 如果有关联图片,可以下载并添加附件
        if let imageURL = userInfo["image_url"] as? String {
            downloadAndAttachImage(url: imageURL, content: modifiedContent) {
                contentHandler(modifiedContent)
            }
        } else {
            contentHandler(modifiedContent)
        }
    }
}

Service Extension 的运行时间限制为 30 秒,超时后系统会展示原始通知内容。

实战案例

案例一:完整的推送注册与处理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import UIKit
import UserNotifications

@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        setupPushNotifications()
        return true
    }
    
    private func setupPushNotifications() {
        let center = UNUserNotificationCenter.current()
        center.delegate = self
        
        // 检查当前授权状态
        center.getNotificationSettings { settings in
            switch settings.authorizationStatus {
            case .notDetermined:
                // 尚未请求,在合适的时机请求
                self.requestPushPermission()
            case .denied:
                // 被拒绝,用户可能不知道推送的好处
                print("推送权限被拒绝")
            case .authorized, .provisional:
                // 已有权限,注册远程通知
                DispatchQueue.main.async {
                    UIApplication.shared.registerForRemoteNotifications()
                }
            @unknown default:
                break
            }
        }
    }
    
    private func requestPushPermission() {
        let center = UNUserNotificationCenter.current()
        center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
            if let error = error {
                print("请求推送权限出错: \(error.localizedDescription)")
                return
            }
            
            if granted {
                DispatchQueue.main.async {
                    UIApplication.shared.registerForRemoteNotifications()
                }
            }
        }
    }
    
    // MARK: - Device Token
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
        print("Device Token: \(token)")
        
        // 将 Token 上传到服务器
        uploadDeviceToken(token)
    }
    
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("推送注册失败: \(error.localizedDescription)")
    }
    
    // MARK: - UNUserNotificationCenterDelegate
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        // 前台收到通知:展示横幅且播放声音
        if #available(iOS 14.0, *) {
            completionHandler([.banner, .sound, .badge, .list])
        } else {
            completionHandler([.alert, .sound, .badge])
        }
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo
        
        // 处理不同类型的响应
        switch response.actionIdentifier {
        case UNNotificationDefaultActionIdentifier:
            // 用户点击了通知主体
            handleNotificationTap(userInfo: userInfo)
        case "reply":
            // 用户点击了"回复"按钮
            handleReplyAction(userInfo: userInfo)
        case "mark_read":
            // 用户点击了"标记已读"
            handleMarkReadAction(userInfo: userInfo)
        default:
            // 自定义操作
            if response.actionIdentifier == UNNotificationDismissActionIdentifier {
                print("用户划走了通知")
            }
        }
        
        completionHandler()
    }
    
    private func uploadDeviceToken(_ token: String) {
        // 将 Token 发送到你的推送服务器
        // ...
    }
    
    private func handleNotificationTap(userInfo: [AnyHashable: Any]) {
        guard let type = userInfo["type"] as? String else { return }
        // 根据推送类型跳转到对应页面
        // ...
    }
}

案例二:处理 Flutter 中的原生推送事件

当使用 Flutter 的 firebase_messaging 插件时,iOS 端需要在 AppDelegate 中转发推送事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// AppDelegate.m(iOS 原生部分)
#import <Flutter/Flutter.h>
#import <UserNotifications/UserNotifications.h>
#import <FirebaseMessaging/FirebaseMessaging.h>

@interface AppDelegate () <UNUserNotificationCenterDelegate, FIRMessagingDelegate>
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 配置 Firebase
    [FIRApp configure];
    
    // 设置推送代理
    [UNUserNotificationCenter currentNotificationCenter].delegate = self;
    
    // 请求权限
    UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
    [[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:authOptions completionHandler:^(BOOL granted, NSError * _Nullable error) {
        if (granted) {
            [application registerForRemoteNotifications];
        }
    }];
    
    // 设置 FCM 代理
    [FIRMessaging messaging].delegate = self;
    
    return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

// 转发 Device Token 到 FCM
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    [FIRMessaging messaging].APNSToken = deviceToken;
    [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}

#pragma mark - FIRMessagingDelegate
- (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSString *)fcmToken {
    // FCM Token 更新
    NSLog(@"FCM Token: %@", fcmToken);
    // 上传到你的服务器
}

#pragma mark - UNUserNotificationCenterDelegate
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
    NSDictionary *userInfo = notification.request.content.userInfo;
    // FCM SDK 会自动处理
    [[FIRMessaging messaging] appDidReceiveMessage:userInfo];
    completionHandler(UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionSound);
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
    [[FIRMessaging messaging] appDidReceiveMessage:response.notification.request.content.userInfo];
    completionHandler();
}

@end

常见问题

Q1:推送通知收不到怎么办?

这是最常见的问题。系统性排查步骤:

  1. 检查网络连接:推送通知需要网络,确保设备可正常联网
  2. 检查推送证书/Key 是否有效:在 Apple Developer Center 检查证书是否过期
  3. 检查 Device Token 是否最新:应用卸载重装后 Token 会变化
  4. 检查推送环境:开发证书推送不到生产环境的应用,反之亦然
  5. 检查 APNs 响应:服务器端检查 APNs 返回的 HTTP 状态码,特别关注 404 和 410
  6. 检查应用权限:在 Settings > 通知中检查应用的通知权限是否开启
  7. 检查应用状态:如果应用在前台且没有实现 willPresent 回调,通知不会展示
  8. 检查静默推送频率:content-available 推送过于频繁会被系统限制
  9. 检查低电量模式:低电量模式下系统会禁止后台活动

Q2:前台收到推送但不展示怎么办?

必须在 UNUserNotificationCenterDelegate 的 willPresent 方法中显式调用 completionHandler 来指定展示方式。如果这个方法没有被实现,或者 completionHandler 传入空选项,前台就不会有任何提示。

Q3:推送通知的 payload 大小限制是多少?

标准的远程推送(Alert 类型)最大为 4KB。如果需要传输更多数据,建议在 payload 中只传递消息 ID 和类型,让应用通过网络 API 获取完整的消息内容。静默推送(content-available)的 payload 也限制在 4KB。

Q4:静默推送(content-available)不触发怎么办?

常见原因:

  • 应用没有 Remote notifications 的后台模式(需在 Capabilities 中开启)
  • 用户禁用了应用的后台应用刷新(Settings > 通用 > 后台应用刷新)
  • 设备处于低电量模式
  • 推送频率过高被系统限制
  • 在 iOS 13+ 中,静默推送的优先级被降低

Q5:推送通知集成在 Flutter 中,如何在原生层处理点击事件?

在 Flutter 中,firebase_messaging 插件已经封装了 iOS 和 Android 的推送处理,但有几种情况需要原生层配合:

  1. 应用未启动时点击通知:需要通过在 AppDelegate 的 didFinishLaunchingWithOptions 中检查 launchOptions,将数据传递给 Flutter
  2. 需要自定义通知展示:如展示媒体附件,需要在原生层实现 Notification Service Extension
  3. 需要复杂的后台处理:如在后台解密消息,需要原生层的静默推送处理

可以通过 Flutter 的 MethodChannel 或 EventChannel 实现原生层和 Flutter 层的通信。

总结

iOS 推送通知是 Apple 生态中一个高度完善的系统,它通过严格的认证机制、权限模型和灵活的通知分类,为开发者提供了从服务器到客户端的完整消息传递链路。对于跨平台开发者而言,理解 iOS 推送通知的核心机制不仅是解决”收不到推送”等问题的前提,也是设计跨平台推送架构的基础。

关键要点回顾:

  • 设备注册:iOS 通过 device token 唯一标识设备+应用的组合
  • 推送认证:推送 Key(Token Authentication)是现代 iOS 推送的最佳实践
  • 权限管理:UNUserNotificationCenter 统一管理通知权限和通知交互
  • payload 设计:合理设计推送数据,区分展示内容和业务数据
  • 前台展示:需要实现 willPresent 代理方法才能在前台展示通知
  • 静默推送:content-available 提供后台唤醒能力,但受系统限制较多

在跨平台框架中,firebase_messaging 是最常用的推送集成方案,它利用 iOS 的 APNs 作为底层通道,通过 FCM 统一管理推送投递。但无论使用哪个库,推送问题排查的终点总是落在 iOS 原生的 APNs 认证和 UNUserNotificationCenter 处理流程上——这正是理解原生推送机制不可替代的价值所在。

本文由作者按照 CC BY 4.0 进行授权