iOS实现消息推送及原理分析
什么是消息推送?
消息推送是指在无需打开应用程序的情况下,向手机用户发送通知消息。消息推送可以通过苹果官方提供的APNs(Apple Push Notification service,苹果推送服务)完成。
APNs的工作原理
APNs与苹果设备之间的通信是基于一种专门为该服务设计的二进制协议,这个协议被称为APNs协议。APNs协议工作的必要前提是设备必须向APNs注册。注册的过程分为三步:
- 应用程序向APNs服务器发送一个请求,表示应用程序想要获取推送通知。
- APNs服务器向设备发送一条唯一的标识该设备的令牌。
- 应用程序将此令牌与设备相关联上传至自己开发的服务器上,推送消息时,APNs服务就需要知道要推送给哪些设备。
注册成功后,当服务器需要推送消息时,它会将要发送的消息及设备令牌一起发送给APNs服务器。APNs接收到消息及令牌后,就会对每个设备进行验证并确定该消息最终使用的传输通道。成功验证并传输过程中,APNs则会将消息推送到设备上。
如何实现消息推送?
在iOS中实现消息推送其实很简单,关键在于对APNs协议的理解和应用。以下是具体步骤:
- 注册开通APNs服务(这里不再赘述)。
- 在项目中引用PushKit框架,并在Capabilities选项卡中开启Push Notifications和Background Modes功能。Programmatic Only和Remote notifications两个选项都要勾选并关联对应的代码。
- 获取设备的DeviceToken:在
AppDelegate.swift
中实现didRegisterForRemoteNotificationsWithDeviceToken
代理方法,并将获取到的DeviceToken上传到服务器上。
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 注册APNs服务
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, error in
if granted == true {
DispatchQueue.main.async {
application.registerForRemoteNotifications()
}
}
}
// Override point for customization after application launch.
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// 发送设备的Token给服务器(这里省略具体实现代码)
}
}
- 发送消息到服务器:将要向设备展示的消息以指定格式(例如JSON)发送到自己的服务端。同时,还需要将对应的设备令牌上传到服务端,并确保服务器与APNs服务器进行了注册和通信以获取对应的令牌。具体消息推送内容和格式可以根据自己的需求进行定制。
- 服务端向APNs服务器发送消息和设备令牌,APNs服务器根据令牌进行验证并推送消息到指定设备。
示例1:APNs本地通知
APNs不仅可以实现远程通知,还可以实现本地推送。本地推送不需要与服务端进行通信,只需要在应用程序中添加本地通知即可。
以下是一个简单的示例代码,可以实现5秒后通知用户一个本地通知。需要注意的是,在XS及以上设备中,会自动弹出通知,但是在较老的设备上,需要在AppDelegate.swift
中实现UNUserNotificationCenterDelegate
代理方法并手动展示通知。
import UIKit
import UserNotifications
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
if granted {
let content = UNMutableNotificationContent()
content.title = "通知标题"
content.body = "通知内容"
content.sound = UNNotificationSound.default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
let request = UNNotificationRequest(identifier: "localNotification", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}
}
return true
}
// xs以上设备会自动弹出通知,较旧的设备需要手动展示通知。
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.sound, .alert])
}
}
示例2:APNs远程推送
下面是一个APNs远程推送的示例代码。服务器端可以使用HTTP/2 推送通知 API 或者是向 APNs 服务器发送HTTP/2 请求发送推送通知,此处我们仅提供单独调用APNs函数的简单代码。
设备端通过苹果官方提供的SDK向APNs服务器注册自己的token,APNs 将提供一个唯一的deviceToken,当有推送通知需要发送给你的 app 时,服务器就会用这个 token 来标识你的 app。
import UIKit
import UserNotifications
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
if granted {
DispatchQueue.global().async {
// 向APNs服务器发送推送消息
let apnsHost = "gateway.push.apple.com"
let apnsPort: Int32 = 2195
let certificatePath = Bundle.main.path(forResource: "certificate", ofType: "p12")
let certificateData = try! Data(contentsOf: URL(fileURLWithPath: certificatePath!))
let options: [String: Any] = [kSecImportExportPassphrase as String: "your_password_here"]
var imported = CFArrayCreate(nil, nil, 0, nil)
let importStatus = SecPKCS12Import(certificateData as CFData, options as CFDictionary, &imported)
guard importStatus == errSecSuccess else {
fatalError()
}
let importArray = imported as! Array<Dictionary<String, Any>>
let identity = importArray[0][kSecImportItemIdentity as String] as! SecIdentity
var cert: UnsafeMutablePointer<Cert>? = nil
cert = useCertificate(identity: identity)
connectAPNs(certificate: cert!, apnsHost: apnsHost, apnsPort: apnsPort)
}
}
}
return true
}
struct Cert {
let identity: SecIdentity
let certificate: SecCertificate
}
func useCertificate(identity: SecIdentity) -> UnsafeMutablePointer<Cert> {
var certificate: SecCertificate?
SecIdentityCopyCertificate(identity, &certificate)
let cert = UnsafseMutablePointer<Cert>.allocate(capacity: 1)
cert.pointee = Cert(identity: identity, certificate: certificate!)
return cert
}
func connectAPNs(certificate: UnsafeMutablePointer<Cert>, apnsHost: String, apnsPort: Int32) {
let sock = openAPNsConnection(host: apnsHost, port: apnsPort, cert: certificate.pointee.certificate, key: certificate.pointee.identity, chain: nil)
let deviceToken = "device_token_here"
let message = "Hello, World!"
let payload = "{\"aps\": {\"alert\": \"\(message)\", \"sound\": \"default\"}}"
var buffer = [UInt8]()
buffer.append(0) // payload 的通用头
buffer.append(0)
buffer.append(32) // deviceToken 长度
buffer.append(contentsOf: deviceToken.hexadecimalToBytes())
buffer.append(0) // payload 的长度
buffer.append(UInt8(payload.utf8.count))
buffer.append(contentsOf: payload.utf8)
write(sock: sock, data: buffer)
close(sock)
}
func openAPNsConnection(host: String, port: Int32, cert: SecCertificate, key: SecIdentity, chain: [SecCertificate]?) -> Int32 {
var result: Int32 = -1
let clientContext = SSLClientContext(cert: cert, key: key, chain: chain)
var inputStream: CFReadStream!
var outputStream: CFWriteStream!
CFStreamCreatePairWithSocketToHost(nil, host as CFString, port, &inputStream, &outputStream)
CFReadStreamSetProperty(inputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext), clientContext)
CFWriteStreamSetProperty(outputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext), clientContext)
if CFReadStreamOpen(inputStream) == true && CFWriteStreamOpen(outputStream) == true {
result = outputStream as! Int32
}
return result
}
func close(_ sock: Int32) {
#if os(iOS) || os(tvOS) || os(watchOS)
_ = Darwin.close(sock)
#elseif os(macOS)
_ = Darwin.close(sock)
#endif
}
func write(sock: Int32, data: [UInt8]) {
if data.isEmpty == false {
var index = 0
repeat {
let written = Darwin.write(sock, data, data.count - index)
// -1 表示写入失败
if written == -1 {
throw NSError(domain: "com.company.app", code: 1, userInfo: nil)
}
index += written
} while index != data.count
}
}
}
结论
APNs是一个十分方便且稳定的消息推送服务。在实现过程中,关键在于对APNs协议的掌握和服务端的编写,每个部分都有其特定的要求和考虑点。在使用PushKit框架时,需要注意权限及背景运行的相关设置,并在实现远程推送时,需要对APNs服务器进行认证和验证,确保推送的消息是合法、可靠的。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:iOS实现消息推送及原理分析 - Python技术站