Firebase Cloud Messaging推送通知实现
概述
Firebase Cloud Messaging(FCM)是Google提供的跨平台消息推送服务,支持iOS、Android和Web应用。它提供了可靠、高效的消息传递机制,让开发者能够向用户设备发送通知和数据消息。
本文将深入探讨如何在iOS应用中集成FCM,实现完整的推送通知功能。
核心概念
FCM架构
关键组件
组件 | 作用 | 说明 |
---|---|---|
FCM Token | 设备标识 | 唯一标识设备,用于定向推送 |
APNs Token | Apple推送服务 | iOS设备与APNs的通信凭证 |
Topics | 主题订阅 | 基于主题的消息广播机制 |
Message Types | 消息类型 | 通知消息和数据消息 |
环境配置
1. 项目设置
首先需要在Firebase控制台创建项目并添加iOS应用:
- 访问 Firebase控制台
- 创建新项目或选择现有项目
- 添加iOS应用,填写Bundle ID
- 下载
GoogleService-Info.plist
文件
2. 证书配置
为了接收推送通知,需要配置APNs证书:
- 在Apple Developer Center创建App ID
- 启用Push Notifications功能
- 生成APNs认证密钥或证书
- 在Firebase控制台上传APNs凭证
代码实现
1. 初始化配置
在 AppDelegate.swift
中配置Firebase和推送通知:
import FirebaseCore
import FirebaseMessaging
import UserNotifications
@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 配置Firebase
FirebaseApp.configure()
// 设置消息代理
Messaging.messaging().delegate = self
// 配置推送通知
configurePushNotifications(application)
return true
}
private func configurePushNotifications(_ application: UIApplication) {
let center = UNUserNotificationCenter.current()
center.delegate = self
// 请求通知权限
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if let error = error {
print("通知权限请求失败: \(error)")
}
}
// 注册远程通知
application.registerForRemoteNotifications()
}
}
2. 处理APNs Token
extension AppDelegate {
// 成功注册APNs时调用
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// 将APNs token设置到Messaging
Messaging.messaging().apnsToken = deviceToken
print("APNs token received: \(deviceToken.reduce("", {$0 + String(format: "%02X", $1)}))")
}
// 注册APNs失败时调用
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("APNs注册失败: \(error)")
}
}
3. 实现MessagingDelegate
extension AppDelegate: MessagingDelegate {
// 接收FCM Token刷新
func messaging(_ messaging: Messaging,
didReceiveRegistrationToken fcmToken: String?) {
guard let token = fcmToken else { return }
print("FCM Token: \(token)")
// 将token发送到您的服务器
sendTokenToServer(token)
}
private func sendTokenToServer(_ token: String) {
// 实现将token发送到您的后端服务器
let url = URL(string: "https://your-server.com/register-device")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"deviceToken": token,
"platform": "ios",
"appVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Token发送失败: \(error)")
} else {
print("Token发送成功")
}
}.resume()
}
}
4. 处理接收到的消息
extension AppDelegate {
// 处理前台通知显示
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
let userInfo = notification.request.content.userInfo
// 处理消息内容
handleMessage(userInfo)
// 即使应用在前台也显示通知
completionHandler([.banner, .sound, .badge])
}
// 处理通知点击
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
// 处理通知点击逻辑
handleNotificationTap(userInfo)
completionHandler()
}
// 处理静默推送和数据消息
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// 记录消息传递指标
Messaging.serviceExtension().exportDeliveryMetricsToBigQuery(withMessageInfo: userInfo)
// 处理数据消息
handleDataMessage(userInfo)
completionHandler(.newData)
}
private func handleMessage(_ userInfo: [AnyHashable: Any]) {
// 解析消息内容
if let aps = userInfo["aps"] as? [String: Any] {
if let alert = aps["alert"] as? [String: String] {
let title = alert["title"] ?? "通知"
let body = alert["body"] ?? ""
print("收到通知: \(title) - \(body)")
}
}
// 处理自定义数据
if let customData = userInfo["customData"] as? [String: Any] {
print("自定义数据: \(customData)")
}
}
}
高级功能
1. 主题订阅
class NotificationManager {
static let shared = NotificationManager()
private init() {}
// 订阅主题
func subscribeToTopic(_ topic: String) {
Messaging.messaging().subscribe(toTopic: topic) { error in
if let error = error {
print("订阅主题失败: \(topic), 错误: \(error)")
} else {
print("成功订阅主题: \(topic)")
}
}
}
// 取消订阅
func unsubscribeFromTopic(_ topic: String) {
Messaging.messaging().unsubscribe(fromTopic: topic) { error in
if let error = error {
print("取消订阅失败: \(topic), 错误: \(error)")
} else {
print("成功取消订阅: \(topic)")
}
}
}
// 批量订阅管理
func manageTopics(subscriptions: [String], unsubscriptions: [String]) {
let group = DispatchGroup()
// 处理订阅
for topic in subscriptions {
group.enter()
subscribeToTopic(topic)
group.leave()
}
// 处理取消订阅
for topic in unsubscriptions {
group.enter()
unsubscribeFromTopic(topic)
group.leave()
}
}
}
2. Token管理
class TokenManager {
static let shared = TokenManager()
// 获取当前FCM Token
func getFCMToken(completion: @escaping (String?) -> Void) {
Messaging.messaging().token { token, error in
if let error = error {
print("获取Token失败: \(error)")
completion(nil)
} else {
completion(token)
}
}
}
// 删除Token
func deleteToken(completion: @escaping (Bool) -> Void) {
Messaging.messaging().deleteToken { error in
if let error = error {
print("删除Token失败: \(error)")
completion(false)
} else {
print("Token删除成功")
completion(true)
}
}
}
// 监控Token刷新
func monitorTokenRefresh() {
NotificationCenter.default.addObserver(
forName: Notification.Name.MessagingRegistrationTokenRefreshed,
object: nil,
queue: .main
) { notification in
self.getFCMToken { token in
if let token = token {
print("Token已刷新: \(token)")
// 更新服务器上的token
self.updateTokenOnServer(token)
}
}
}
}
}
3. 消息处理工具类
struct FCMMessage {
let title: String?
let body: String?
let sound: String?
let badge: Int?
let customData: [String: Any]
let messageType: MessageType
enum MessageType {
case notification
case data
case silent
}
init?(userInfo: [AnyHashable: Any]) {
guard let aps = userInfo["aps"] as? [String: Any] else {
return nil
}
// 解析通知内容
if let alert = aps["alert"] as? [String: String] {
self.title = alert["title"]
self.body = alert["body"]
} else if let alert = aps["alert"] as? String {
self.title = alert
self.body = nil
} else {
self.title = nil
self.body = nil
}
self.sound = aps["sound"] as? String
self.badge = aps["badge"] as? Int
// 解析自定义数据
var customData = userInfo
customData.removeValue(forKey: "aps")
self.customData = customData
// 判断消息类型
if aps["content-available"] as? Int == 1 {
self.messageType = .silent
} else if self.title != nil || self.body != nil {
self.messageType = .notification
} else {
self.messageType = .data
}
}
}
class MessageHandler {
static func handleMessage(_ userInfo: [AnyHashable: Any]) {
guard let message = FCMMessage(userInfo: userInfo) else {
print("无法解析消息内容")
return
}
switch message.messageType {
case .notification:
handleNotificationMessage(message)
case .data:
handleDataMessage(message)
case .silent:
handleSilentMessage(message)
}
}
private static func handleNotificationMessage(_ message: FCMMessage) {
print("处理通知消息: \(message.title ?? "无标题")")
// 这里可以添加具体的业务逻辑
if let deepLink = message.customData["deepLink"] as? String {
handleDeepLink(deepLink)
}
}
private static func handleDataMessage(_ message: FCMMessage) {
print("处理数据消息")
// 处理应用内数据更新
if let updateType = message.customData["updateType"] as? String {
switch updateType {
case "userProfile":
updateUserProfile(from: message.customData)
case "config":
updateAppConfig(from: message.customData)
default:
break
}
}
}
private static func handleSilentMessage(_ message: FCMMessage) {
print("处理静默消息")
// 后台数据同步
if let syncType = message.customData["syncType"] as? String {
BackgroundSyncManager.syncData(type: syncType)
}
}
}
错误处理与调试
1. 错误处理
enum FCMError: Error, LocalizedError {
case tokenFetchFailed(Error)
case tokenDeleteFailed(Error)
case subscriptionFailed(String, Error)
case unsubscriptionFailed(String, Error)
case permissionDenied
case apnsRegistrationFailed
var errorDescription: String? {
switch self {
case .tokenFetchFailed(let error):
return "Token获取失败: \(error.localizedDescription)"
case .tokenDeleteFailed(let error):
return "Token删除失败: \(error.localizedDescription)"
case .subscriptionFailed(let topic, let error):
return "订阅主题失败(\(topic)): \(error.localizedDescription)"
case .unsubscriptionFailed(let topic, let error):
return "取消订阅失败(\(topic)): \(error.localizedDescription)"
case .permissionDenied:
return "用户拒绝了通知权限"
case .apnsRegistrationFailed:
return "APNs注册失败"
}
}
}
class ErrorHandler {
static func handleFCMError(_ error: Error) {
if let fcmError = error as? FCMError {
print("FCM错误: \(fcmError.errorDescription ?? "未知错误")")
// 根据错误类型采取不同的处理策略
switch fcmError {
case .permissionDenied:
showPermissionAlert()
case .tokenFetchFailed:
scheduleTokenRetry()
default:
break
}
} else {
print("未知错误: \(error.localizedDescription)")
}
}
private static func showPermissionAlert() {
// 显示引导用户开启通知权限的提示
DispatchQueue.main.async {
if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = scene.windows.first?.rootViewController {
let alert = UIAlertController(
title: "通知权限被拒绝",
message: "请到设置中开启通知权限以接收重要消息",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
})
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
rootVC.present(alert, animated: true)
}
}
}
}
2. 调试工具
class FCMDebugger {
static var isDebugMode = false
static func log(_ message: String, level: LogLevel = .info) {
guard isDebugMode else { return }
let timestamp = DateFormatter.localizedString(
from: Date(),
dateStyle: .none,
timeStyle: .medium
)
let logMessage = "[FCM][\(timestamp)][\(level.rawValue)] \(message)"
switch level {
case .error:
print("❌ \(logMessage)")
case .warning:
print("⚠️ \(logMessage)")
case .info:
print("ℹ️ \(logMessage)")
case .debug:
print("🔍 \(logMessage)")
}
}
enum LogLevel: String {
case error = "ERROR"
case warning = "WARNING"
case info = "INFO"
case debug = "DEBUG"
}
static func dumpMessageInfo(_ userInfo: [AnyHashable: Any]) {
guard isDebugMode else { return }
log("收到消息内容:", level: .debug)
for (key, value) in userInfo {
log(" \(key): \(value)", level: .debug)
}
}
}
最佳实践
1. 性能优化
class FCMPerformanceOptimizer {
// 批量操作延迟
private static let batchDelay: TimeInterval = 0.5
private static var pendingOperations: [() -> Void] = []
private static var batchTimer: Timer?
// 批量订阅操作
static func batchSubscribe(to topics: [String]) {
topics.forEach { topic in
addOperation {
Messaging.messaging().subscribe(toTopic: topic)
}
}
scheduleBatchExecution()
}
// 批量取消订阅
static func batchUnsubscribe(from topics: [String]) {
topics.forEach { topic in
addOperation {
Messaging.messaging().unsubscribe(fromTopic: topic)
}
}
scheduleBatchExecution()
}
private static func addOperation(_ operation: @escaping () -> Void) {
pendingOperations.append(operation)
}
private static func scheduleBatchExecution() {
batchTimer?.invalidate()
batchTimer = Timer.scheduledTimer(
withTimeInterval: batchDelay,
repeats: false
) { _ in
executeBatch()
}
}
private static func executeBatch() {
let operations = pendingOperations
pendingOperations.removeAll()
operations.forEach { $0() }
}
}
2. 安全考虑
class FCMSecurityManager {
// 验证消息来源
static func verifyMessageSource(_ userInfo: [AnyHashable: Any]) -> Bool {
// 检查消息是否包含预期的签名或标识
// 这里可以添加自定义的验证逻辑
guard let from = userInfo["from"] as? String else {
return false
}
// 验证发送者是否为可信来源
let trustedSenders = ["your-firebase-project-id"]
return trustedSenders.contains(from)
}
// 加密敏感数据
static func encryptSensitiveData(_ data: [String: Any]) -> [String: Any] {
var encryptedData = data
// 对敏感字段进行加密处理
if let sensitiveInfo = data["sensitive"] as? String {
encryptedData["sensitive"] = encryptString(sensitiveInfo)
}
return encryptedData
}
private static func encryptString(_ string: String) -> String {
// 实现加密逻辑,这里使用Base64示例
return Data(string.utf8).base64EncodedString()
}
}
测试策略
1. 单元测试
import XCTest
@testable import YourApp
class FCMTests: XCTestCase {
var messaging: Messaging!
var notificationCenter: UNUserNotificationCenter!
override func setUp() {
super.setUp()
messaging = Messaging.messaging()
notificationCenter = UNUserNotificationCenter.current()
}
func testTokenRetrieval() {
let expectation = self.expectation(description: "Token retrieval")
messaging.token { token, error in
XCTAssertNil(error, "Token获取不应失败")
XCTAssertNotNil(token, "应返回有效的Token")
XCTAssertTrue(token?.isEmpty == false, "Token不应为空")
expectation.fulfill()
}
waitForExpectations(timeout: 10, handler: nil)
}
func testTopicSubscription() {
let testTopic = "testTopic"
let expectation = self.expectation(description: "Topic subscription")
messaging.subscribe(toTopic: testTopic) { error in
XCTAssertNil(error, "主题订阅不应失败")
// 验证订阅是否成功
// 这里可以添加验证逻辑
expectation.fulfill()
}
waitForExpectations(timeout: 10, handler: nil)
}
}
2. 集成测试
class FCMIntegrationTests: XCTestCase {
func testEndToEndMessageFlow() {
// 模拟接收推送消息
let testMessage: [AnyHashable: Any] = [
"aps": [
"alert": [
"title": "测试标题",
"body": "测试内容"
],
"sound": "default"
],
"customData": [
"deepLink": "app://test/page",
"timestamp": Date().timeIntervalSince1970
]
]
// 模拟应用收到消息
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.application(
UIApplication.shared,
didReceiveRemoteNotification: testMessage,
fetchCompletionHandler: { result in
XCTAssertEqual(result, .newData, "应返回新数据")
}
)
// 验证消息处理结果
// 这里可以添加具体的验证逻辑
}
}
总结
Firebase Cloud Messaging为iOS应用提供了强大而灵活的推送通知解决方案。通过本文的详细指南,您应该能够:
- 正确配置 FCM环境和APNs证书
- 实现完整的 消息接收和处理流程
- 管理设备Token 和主题订阅
- 处理各种类型的 推送消息
- 实施最佳实践 确保性能和安全性
记住,良好的推送通知策略应该:
- 尊重用户的选择和隐私
- 提供有价值的内容
- 优化性能和电池使用
- 包含适当的错误处理和监控
通过遵循这些指导原则,您可以为用户提供出色的推送通知体验,同时确保应用的稳定性和可靠性。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考