級(jí)別: ★★☆☆☆
標(biāo)簽:「iOS通知」「iOSPush」「遠(yuǎn)程通知」
作者: dac_1033
審校: QiShare團(tuán)隊(duì)
iOS中的通知(Notification)分為兩種:
1. iOS 本地通知
2. iOS 遠(yuǎn)程通知
3. iOS 通知擴(kuò)展
iOS中的通知包括本地通知和遠(yuǎn)程通知,兩種通知在iOS系統(tǒng)中通過(guò)橫幅或者彈出提醒兩種形式來(lái)告訴用戶,點(diǎn)擊系統(tǒng)彈出的通知會(huì)打開(kāi)應(yīng)用程序。
今天主要介紹iOS端關(guān)于遠(yuǎn)程通知的相關(guān)功能及操作。
遠(yuǎn)程通知
遠(yuǎn)程通知是通過(guò)蘋(píng)果的APNs(
Apple Push Notification server)發(fā)送到App,而APNs必須先知道用戶設(shè)備的地址,然后才能向該設(shè)備發(fā)送通知。此地址采用設(shè)備令牌的形式,該設(shè)備令牌對(duì)于設(shè)備和應(yīng)用程序都是唯一的(即device token)。在啟動(dòng)時(shí),App與APNs通信并接收device token,然后將其轉(zhuǎn)發(fā)到App Server,App Server將包含該令牌及要發(fā)送的通知消息發(fā)送至APNs。(蘋(píng)果官網(wǎng)APNs概述)
遠(yuǎn)程通知的傳遞涉及幾個(gè)關(guān)鍵組件:
- App Server
- Apple推送通知服務(wù)(APNs)
- 用戶的設(shè)備(包括iPhone、iPad、iTouch、mac等)
- 相應(yīng)的App
蘋(píng)果官方提供的遠(yuǎn)程通知的傳遞示意圖如下:

遠(yuǎn)程通知中各關(guān)鍵組件之間的交互細(xì)節(jié):

準(zhǔn)備工作:
(1)在蘋(píng)果開(kāi)發(fā)者賬號(hào)中創(chuàng)建的App ID不能使用通配ID,并且在所創(chuàng)建的APP ID的配置項(xiàng)中選擇Push Notifications服務(wù),App使用沒(méi)有選擇該服務(wù)的App ID所生成的推送證書(shū)和配置文件時(shí),無(wú)法完成注冊(cè)遠(yuǎn)程通知;
(2)當(dāng)前工程配置中的Bundle Identifier必須和生成配置文件使用的APP ID完全一致;
(3)當(dāng)前工程配置中的“Capabilities”需設(shè)置為ON;
(4)遠(yuǎn)程推送必須真機(jī)調(diào)試,模擬器無(wú)法獲取得到device token。
詳細(xì)步驟:
- 在AppDelegate中注冊(cè)APNs消息
-(void)registerRemoteNotification {
if ([[UIDevice currentDevice].systemVersion floatValue] >= 10.0) {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 // Xcode 8編譯會(huì)調(diào)用
if (@available(iOS 10.0, *)) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = self;
[center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionCarPlay) completionHandler:^(BOOL granted, NSError *_Nullable error) {
if (!error) {
NSLog(@"request notification authorization succeeded!");
}
}];
}
[[UIApplication sharedApplication] registerForRemoteNotifications];
#else // Xcode 7編譯會(huì)調(diào)用
UIUserNotificationType types = (UIUserNotificationTypeAlert | UIUserNotificationTypeSound | UIUserNotificationTypeBadge);
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];
[[UIApplication sharedApplication] registerForRemoteNotifications];
#endif
} else {
UIUserNotificationType types = (UIUserNotificationTypeAlert | UIUserNotificationTypeSound | UIUserNotificationTypeBadge);
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];
[[UIApplication sharedApplication] registerForRemoteNotifications];
}
}
- App獲取
device token
- 在注冊(cè)遠(yuǎn)程通知之后,獲取
device token成功回調(diào):
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken;
- 獲取
device token失?。?/li>
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error;
App將
device token發(fā)送給App Server
只有蘋(píng)果公司知道device token的生成算法,保證唯一,device token在App重裝等情況時(shí)會(huì)變化,因此為確保device token變化后App仍然能夠正常接收服務(wù)器端發(fā)送的通知,建議每次應(yīng)用程序啟動(dòng)都重新獲得device token,并傳給App Server。App Server根據(jù)最新的
device token將要推送的消息發(fā)送給APNs
將指定device token和消息內(nèi)容發(fā)送給APNs時(shí),消息內(nèi)容的格式必須完全按照蘋(píng)果官方的消息格式組織消息內(nèi)容,點(diǎn)擊查看遠(yuǎn)程通知消息的字段、創(chuàng)建遠(yuǎn)程通知消息。
消息格式的例子如下:
{"aps":{"alert":{"title":"通知的title","subtitle":"通知的subtitle","body":"通知的body","title-loc-key":"TITLE_LOC_KEY","title-loc-args":["t_01","t_02"],"loc-key":"LOC_KEY","loc-args":["l_01","l_02"]},"sound":"sound01.wav","badge":1,"mutable-content":1,"category": "realtime"},"msgid":"123"}
APNs根據(jù)device token查找相應(yīng)設(shè)備,并推送消息
一般情況APNs可以根據(jù)deviceToken將消息成功推送到相應(yīng)設(shè)備中,但也存在用戶卸載程序等原因?qū)е峦扑拖⑹〉那闆r,這時(shí)App服務(wù)端會(huì)收到APNs返回的錯(cuò)誤信息)。AppDelegate.m中的回調(diào)方法
// iOS<10時(shí),且app被完全殺死
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
// 注:iOS10以上,如果不使用UNUserNotificationCenter,將走此回調(diào)方法
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo;
// iOS7及以上系統(tǒng)
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler;
// iOS>=10: App在前臺(tái)獲取到通知
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler;
// iOS>=10: 點(diǎn)擊通知進(jìn)入App時(shí)觸發(fā)(殺死/切到后臺(tái)喚起)
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler;
注冊(cè)遠(yuǎn)程通知及解析通知數(shù)據(jù)的代碼如下:
#import "AppDelegate.h"
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
#import <UserNotifications/UserNotifications.h>
#endif
@interface AppDelegate () <UNUserNotificationCenterDelegate>
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 注冊(cè)APNs
[self registerRemoteNotifications];
return YES;
}
- (void)registerRemoteNotifications {
if ([[UIDevice currentDevice].systemVersion floatValue] >= 10.0) {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 // Xcode 8編譯會(huì)調(diào)用
if (@available(iOS 10.0, *)) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = self;
[center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionCarPlay) completionHandler:^(BOOL granted, NSError *_Nullable error) {
if (!error) {
NSLog(@"request authorization succeeded!");
}
}];
} else {
// Fallback on earlier versions
}
[[UIApplication sharedApplication] registerForRemoteNotifications];
#else // Xcode 7編譯會(huì)調(diào)用
UIUserNotificationType types = (UIUserNotificationTypeAlert | UIUserNotificationTypeSound | UIUserNotificationTypeBadge);
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];
[[UIApplication sharedApplication] registerForRemoteNotifications];
#endif
} else if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) {
UIUserNotificationType types = (UIUserNotificationTypeAlert | UIUserNotificationTypeSound | UIUserNotificationTypeBadge);
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];
[[UIApplication sharedApplication] registerForRemoteNotifications];
} else {
UIRemoteNotificationType apn_type = (UIRemoteNotificationType)(UIRemoteNotificationTypeAlert |
UIRemoteNotificationTypeSound |
UIRemoteNotificationTypeBadge);
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:apn_type];
}
}
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
// 獲取并處理deviceToken
NSString *token = [[deviceToken description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];
token = [token stringByReplacingOccurrencesOfString:@" " withString:@""];
DLog(@"---DeviceToken--->> %@\n", token);
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
DLog(@"---register RemoteNotifications failed---\n%@", error);
}
// 注:iOS10以上,如果不使用UNUserNotificationCenter,將走此回調(diào)方法
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
// iOS6及以下系統(tǒng)
if (userInfo) {
if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {// app位于前臺(tái)通知
NSLog(@"app位于前臺(tái)通知(didReceiveRemoteNotification:):%@", userInfo);
} else {// 切到后臺(tái)喚起
NSLog(@"app位于后臺(tái)通知(didReceiveRemoteNotification:):%@", userInfo);
}
}
}
// 注:
// 1. 該回調(diào)方法,App殺死后并不執(zhí)行;
// 2. 該回調(diào)方法,會(huì)與application:didReceiveRemoteNotification:互斥執(zhí)行;
// 3. 該回調(diào)方法,會(huì)與userNotificationCenter:willPresentNotification:withCompletionHandler:一并執(zhí)行;
// 4. 該回調(diào)方法,會(huì)與userNotificationCenter:didReceiveNotificationResponse::withCompletionHandler:一并執(zhí)行。
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler NS_AVAILABLE_IOS(7_0) {
// iOS7及以上系統(tǒng)
if (userInfo) {
if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {// app位于前臺(tái)通知
NSLog(@"app位于前臺(tái)通知(didReceiveRemoteNotification:fetchCompletionHandler:):%@", userInfo);
} else {// 切到后臺(tái)喚起
NSLog(@"app位于后臺(tái)通知(didReceiveRemoteNotification:fetchCompletionHandler:):%@", userInfo);
}
}
completionHandler(UIBackgroundFetchResultNewData);
}
#pragma mark - iOS>=10 中收到推送消息
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
// iOS>=10: App在前臺(tái)獲取到通知
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
API_AVAILABLE(ios(10.0)) {
NSDictionary * userInfo = notification.request.content.userInfo;
if (userInfo) {
NSLog(@"app位于前臺(tái)通知(willPresentNotification:):%@", userInfo);
}
completionHandler(UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionSound|UNNotificationPresentationOptionAlert);;
}
// iO>=10: 點(diǎn)擊通知進(jìn)入App時(shí)觸發(fā)(殺死/切到后臺(tái)喚起)
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
API_AVAILABLE(ios(10.0)) {
NSDictionary * userInfo = response.notification.request.content.userInfo;
if (userInfo) {
NSLog(@"點(diǎn)擊通知進(jìn)入App時(shí)觸發(fā)(didReceiveNotificationResponse:):%@", userInfo);
}
completionHandler();
}
#endif
@end
- 模擬推送工具“Pusher”
本文只側(cè)重于介紹iOS端對(duì)遠(yuǎn)程推送通知的處理,因此我們把App Server對(duì)應(yīng)的處理過(guò)程交給了第三方工具,第三方推送測(cè)試工具有很多,如SmartPush、Pusher等,在這里我們選用Pusher作為測(cè)試工具,Pusher的GitHub地址。
Pusher截圖
Pusher的使用步驟說(shuō)明:
(1)選擇p12格式的推送證書(shū);
(2)設(shè)置是否為測(cè)試環(huán)境(默認(rèn)勾選為測(cè)試環(huán)境,由于推送證書(shū)分為測(cè)試推送證書(shū)和生產(chǎn)測(cè)試證書(shū),并且蘋(píng)果的APNs也分為測(cè)試和生產(chǎn)兩套環(huán)境,因此Pusher需要手動(dòng)置頂是否為測(cè)試環(huán)境);
(3)輸入device token;
(4)輸入符合蘋(píng)果要求的推送內(nèi)容字符串;
(5)當(dāng)確認(rèn)手機(jī)端設(shè)置無(wú)誤,并且以上4點(diǎn)設(shè)置正確時(shí),執(zhí)行推送。
Pusher推送的消息,以第4點(diǎn)中的示例為例進(jìn)行測(cè)試,手機(jī)收到遠(yuǎn)程推送通知的效果截圖如下:

點(diǎn)擊遠(yuǎn)程推送通知橫幅打開(kāi)App,在回調(diào)中獲取的
json串:
備注:
(1)要使用APNs向非運(yùn)行的應(yīng)用程序提供遠(yuǎn)程通知,需要至少啟動(dòng)目標(biāo)應(yīng)用程序一次;
(2)設(shè)備沒(méi)有網(wǎng)絡(luò)的情況下,是無(wú)法注冊(cè)遠(yuǎn)程通知的;
(3)一般情況下,device token是不會(huì)發(fā)生變化的,即雖然調(diào)用注冊(cè)遠(yuǎn)程通知的方法,但是返回的device token仍然是之前得到的值;如果設(shè)備令牌在應(yīng)用程序執(zhí)行時(shí)發(fā)生更改,則應(yīng)用程序?qū)ο笤俅握{(diào)用相應(yīng)的委托方法以通知更改;
(4)推送過(guò)程中的消息json串可在適當(dāng)位置添加自定義字段,整個(gè)消息最大長(zhǎng)度為4 KB(4096字節(jié)),超過(guò)最大允許長(zhǎng)度,則拒絕通知;
(5)在iOS及以上系統(tǒng)中遠(yuǎn)程通知還包括“通知擴(kuò)展”功能,在下一篇文章中介紹。
本文Demo鏈接:GitHub地址
專(zhuān)欄下一篇:iOS 通知擴(kuò)展
了解更多iOS及相關(guān)新技術(shù),請(qǐng)關(guān)注我們的公眾號(hào):

關(guān)注我們的途徑有:
QiShare(簡(jiǎn)書(shū))
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公眾號(hào))
