前言
現(xiàn)在第三方推送也很多 ,比如極光,融云,信鴿,其原理也是相同利用APNS推送機制 ,公司讓做自己的推送。
- 避免device token被第三方泄露,保護手機設(shè)備信息;
- 第三方部分開始收費,一些免費的以后誰又知道呢 ,不如自己創(chuàng)建,除了后臺麻煩點,需要數(shù)據(jù)庫來存儲token相關(guān)字段(后臺配置部分在最后更新添加),前端實現(xiàn)起來并不復(fù)雜。
- 現(xiàn)有的第三方推送一般都只集成有APNS推送,對于VoIP部分并沒有支持,而且信鴿的會導(dǎo)致VoIP消息收不到,網(wǎng)上很多都會說zeropush中有提供VoIP推送,嘗試了下,感覺不怎么好集成,所以不如自己做個推送。
- 對于iOS8.0以后,原生推送實時性很好,這篇文章參考許多資料將詳細的過程講解它 按照一步一步來很容易實現(xiàn) 。
1. 簡介
1.1 推送機制介紹
首先我們看一下蘋果官方給出的對ios推送機制的解釋。如下圖

- Provider就是我們自己程序的后臺服務(wù)器
- APNS是Apple Push Notification Service的縮寫,也就是蘋果的推送服務(wù)器。
上圖可以分為三個階段:
- 第一階段:應(yīng)用程序的服務(wù)器端把要發(fā)送的消息、目的iPhone的標(biāo)識打包,發(fā)給APNS。
- 第二階段:APNS在自身的已注冊Push服務(wù)的iPhone列表中,查找有相應(yīng)標(biāo)識的iPhone,并把消息發(fā)送到iPhone。
- 第三階段:iPhone把發(fā)來的消息傳遞給相應(yīng)的應(yīng)用程序,并且按照設(shè)定彈出Push通知。
APNS推送通知的詳細工作流程
下面這張圖是說明APNS推送通知的詳細工作流程:

根據(jù)圖片我們可以概括一下:
- 應(yīng)用程序注冊APNS消息推送。
- iOS從APNS Server獲取devicetoken,應(yīng)用程序接收device token。
- 應(yīng)用程序?qū)evice token發(fā)送給程序的PUSH服務(wù)端程序。
- 服務(wù)端程序向APNS服務(wù)發(fā)送消息。
- APNS服務(wù)將消息發(fā)送給iPhone應(yīng)用程序。
1.2 推送類型介紹
本地推送(注冊本地推送,比如鬧鐘)
遠程推送(由蘋果APNS服務(wù)器推送給設(shè)備的消息)
- 普通推送(常見的通知欄消息)
- 靜默推送(可以讓App在后臺執(zhí)行一段代碼,熱修復(fù)的原理)
- VoIP推送(可以直接啟動App,然后執(zhí)行對應(yīng)的消息)
- 只有當(dāng)VoIP發(fā)生推送時,設(shè)備才會喚醒,從而節(jié)省能源。
- VoIP推送被認(rèn)為是高優(yōu)先級通知,并且毫無延遲地傳送。
- VoIP推送可以包括比標(biāo)準(zhǔn)推送通知提供的數(shù)據(jù)更多的數(shù)據(jù)。
- 如果收到VoIP推送時,您的應(yīng)用程序未運行,則會自動重新啟動。
- 即使您的應(yīng)用在后臺運行,您的應(yīng)用也會在運行時處理推送。
- VoIP例子
- 關(guān)閉程序后仍可收到推送
- 自定義鈴聲
- 持續(xù)震動
- 呼叫方掛機后,被叫方馬上停止通知鈴聲,并在通知欄沒有留下痕跡
1.3 三方推送平臺介紹
- 信鴿推送(可能會收不到VoIP推送消息,猜測原因與信鴿需要的證書有關(guān))
- 極光推送
- 融云推送
- 網(wǎng)易云推送(不過要集成整個網(wǎng)易云IM)
登錄平臺的官網(wǎng)都會有詳細的集成介紹
1.4 VoIP上架問題
當(dāng)app要上傳App Store時,請在iTunes connect上傳頁面右下角備注中填寫你用到VoIP推送的原因,附加上音視頻呼叫用到VoIP推送功能的demo演示鏈接,演示demo必須提供呼出和呼入功能。
2. 推送證書的制作
考慮到篇幅長度問題,如果對證書制作不熟悉的同學(xué)可以看這篇
iOS推送證書制作
3. 推送客戶端實現(xiàn)
3.1 APNS推送
在xcode -> Capabilities中開啟通知服務(wù)


3.1.1 注冊通知
iOS10以上的需要引入頭文件
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
#import <UserNotifications/UserNotifications.h>
@interface AppDelegate() <UNUserNotificationCenterDelegate>
@end
#endif
#pragma mark - -- 注冊通知
- (void)registerAPNS
{
float sysVer = [[[UIDevice currentDevice] systemVersion] floatValue];
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
if (sysVer >= 10)
{
// iOS 10
[self registerPush10];
}
else if (sysVer >= 8)
{
// iOS 8-9
[self registerPush8to9];
}
else {
// before iOS 8
[self registerPushBefore8];
}
#else
if (sysVer < 8)
{
// before iOS 8
[self registerPushBefore8];
}
else
{
// iOS 8-9
[self registerPush8to9];
}
#endif
}
- (void)registerPush10{
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = self;
[center requestAuthorizationWithOptions:UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted) {
}
}];
[[UIApplication sharedApplication] registerForRemoteNotifications];
#endif
}
- (void)registerPush8to9
{
UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert;
UIUserNotificationSettings *mySettings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];
[[UIApplication sharedApplication] registerForRemoteNotifications];
}
- (void)registerPushBefore8
{
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)];
}
3.1.2 實現(xiàn)通知代理
- 注冊遠程推送
在注冊成功的回調(diào)中會返回deviceToken,這一個token是用于APNS通知的的deviceToken,返回的deviceToken中包含<、>、空格,需要調(diào)一下方法去掉這些字符。如果是用的三方推送,則直接調(diào)三方推送提供的接口上報deviceToken即可,sdk內(nèi)部會處理這些字符。
/**
* 注冊遠程推送
*
* @param application UIApplication 實例
* @param deviceToken 設(shè)備唯一標(biāo)識符
*/
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
NSString *apnsToken = [[[[deviceToken description] stringByReplacingOccurrencesOfString:@"<"withString:@""]
stringByReplacingOccurrencesOfString:@">" withString:@""]
stringByReplacingOccurrencesOfString:@" " withString:@""];
self.apnsToken = apnsToken;
WCLog(@"【APNS】token is : %@", apnsToken);
//update deviceToken
}
/**
* 注冊遠程推送失敗回調(diào)
*
* @param application application description
* @param error 注冊遠程推送失敗錯誤信息
*/
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
WCLog(@"【APNS】register APNS fail.\n【APNS】 reason : %@", error);
}
- iOS10之前收到通知的回調(diào)
#if __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_10_0
/**
* 收到通知的回調(diào)(iOS10之后廢棄)
*
* @param application UIApplication 實例
* @param userInfo 推送時指定的參數(shù)
*/
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo
{
}
#endif
- iOS10之后收到通知的回調(diào)
// iOS 10 新增 API
// iOS 10 會走新 API, iOS 10 以前會走到老 API
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
// App 用戶點擊通知的回調(diào)
// 無論本地推送還是遠程推送都會走這個回調(diào)
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler
{
//TODO... handle notification with type
}
// App 在前臺彈通知需要調(diào)用這個接口
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
/**
* 這個 completionHandle 是回傳給 App 的參數(shù)
*
* @param UNNotificationPresentationOptionAlert 傳了哪個就表示哪兒生效
*
* @return return value
*/
/**
* 如果是在前臺的時候收到通話邀請,則只需要有聲音即可
*/
completionHandler(UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert);
}
#endif
- 收到靜默推送的回調(diào)
除了可以用VoIP啟動程序,iOS10之后蘋果引入的CallKit框架提供與原生通話相同級別的體驗,所以可以在這里使用CallKit框架來調(diào)用來電呼入
/**
* 收到靜默推送的回調(diào)
*
* @param application UIApplication 實例
* @param userInfo 推送時指定的參數(shù)
* @param completionHandler 完成回調(diào)
*/
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSLog(@"[APNS] userinfo %@", userInfo);
//TODO...
completionHandler(UIBackgroundFetchResultNewData);
}
3.1.3 添加本地通知
這里就直接貼項目中使用的代碼,對于通知的實現(xiàn)都放在分類PushService中
.h文件
#import "AppDelegate.h"
@interface AppDelegate (PushService)
/**
* 本地通知
*/
@property (nonatomic, strong) UILocalNotification *localNotify;
@end
.m文件
#import "AppDelegate+PushService.h"
int netCount = 0;
#define MAXNETCOUNT 6
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
#import <UserNotifications/UserNotifications.h>
@interface AppDelegate() <UNUserNotificationCenterDelegate>
@property (nonatomic, strong) UNNotificationRequest *notifyRequest;
@end
#endif
這里用到的Runtime來實現(xiàn)動態(tài)添加屬性,不熟的同學(xué)可以自己去網(wǎng)上搜搜,很多資源,推薦一篇美團寫的博文,很詳細鏈接
#pragma mark - -- 動態(tài)添加屬性
- (void)setLocalNotify:(UILocalNotification *)localNotify
{
objc_setAssociatedObject(self, @"localNotify", localNotify, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UILocalNotification *)localNotify
{
return objc_getAssociatedObject(self, @"localNotify");
}
- (void)setNotifyRequest:(UNNotificationRequest *)notifyRequest
{
objc_setAssociatedObject(self, @"notifyRequest", notifyRequest, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UNNotificationRequest *)notifyRequest
{
return objc_getAssociatedObject(self, @"notifyRequest");
}
發(fā)送本地通知,這里用來做測試的通知。有一個要說的,在文章開始介紹VoIP推送的例子中最后一條呼叫方掛機后,被叫方馬上停止通知鈴聲,并在通知欄沒有留下痕跡,可以在App將要進入前臺的代理中移除對應(yīng)的通知即可
#pragma mark - -- 推送服務(wù)
int notifyCount = 0;
- (void)psSendLocalNotify:(NSString *)notifyText
{
#if DEBUG
notifyCount += 1;
if (IOS_VERSION_OR_LATER(10)) {
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
content.body = [NSString localizedUserNotificationStringForKey:[NSString stringWithFormat:@"%@ - %d", notifyText, notifyCount]
arguments:nil];;
UNTimeIntervalNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats:false];
self.notifyRequest = [UNNotificationRequest requestWithIdentifier:[NSString stringWithFormat:@"localNotify%d", notifyCount]
content:content trigger:trigger];
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:self.notifyRequest withCompletionHandler:^(NSError * _Nullable error) {
}];
} else {
self.localNotify = [[UILocalNotification alloc] init];
self.localNotify.alertBody = notifyText;
[[UIApplication sharedApplication] presentLocalNotificationNow:self.localNotify];
}
#endif
}
移除本地通知,這里移除的都是上個方法中對應(yīng)的通知,如果是用的測試的Identifiers是移除不了的,下面的這個方法只能移除Identifier == @"Voip_Push"的通知,使用的時候要注意
if (IOS_VERSION_OR_LATER(10)) {
[[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:@[@"Voip_Push"]];
} else {
[[UIApplication sharedApplication] cancelLocalNotification:self.localNotify];
}
3.2 VoIP推送
這里很重要,總結(jié)了一些在開發(fā)過程中遇到的坑
老的xcode版本開啟VoIP功能也是在 Background Modes 中直接勾選就開啟了,但是新版的xcode移除了這個選項,所以只能在 info.plist 文件中去手動添加

其實在
Background Modes 勾選上的功能也是要加到 info.plist 中才有效的
在使用<PushKit/PushKit.h>之前,我要說個很重要的問題。就APNS推送給我們的推送開發(fā)思維來說,我們習(xí)慣將處理推送的代碼都放在 AppDelegate 中實現(xiàn),這個確實沒毛病,因為上面實現(xiàn)的那些通知代理都是在

<PushKit/PushKit.h>這個框架可以在程序的任何地方實現(xiàn),任何你業(yè)務(wù)邏輯需求的地方,就算不在 Appdelegate 中注冊,照樣可以通過推送喚醒程序。所以要靈活的使用這個框架,不要被定向開發(fā)思維所禁錮。下面來說說<PushKit/PushKit.h>的實現(xiàn)
VoIP實現(xiàn)
- 引入PushKit庫并遵循代理
#import <PushKit/PushKit.h>
@interface MainTabBarController()<PKPushRegistryDelegate>
@end
- 初始化pushkit推送服務(wù)
#pragma mark - -- 初始化VoIP推送服務(wù)
- (void)loadVoipPush
{
PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
pushRegistry.delegate = self;
pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
[self addApplicationListener];
}
- 實現(xiàn)pushkit代理
上面在APNS中也有獲取過一個deviceToken,這里系統(tǒng)又返回一個deviceToken,這兩個deviceToken是不一樣的,發(fā)送對應(yīng)的通知就要使用對應(yīng)的deviceToken,同樣需要上報給服務(wù)器,由服務(wù)端記錄并發(fā)送通知。
對于接受pushkit消息的回調(diào),蘋果在iOS11之后做了方法更新,僅僅是加了個處理成功的回調(diào),這個回調(diào)我沒用到,所以就都集成到同一個方法了。
#pragma mark - -- VoIP推送代理
- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)pushCredentials forType:(PKPushType)type
{
if ([pushCredentials.token length] == 0) {
NSLog(@"voip token null");
return ;
}
//應(yīng)用啟動獲取token,并上傳服務(wù)器
NSString *voipToken = [[[[pushCredentials.token description] stringByReplacingOccurrencesOfString:@"<"withString:@""]
stringByReplacingOccurrencesOfString:@">" withString:@""]
stringByReplacingOccurrencesOfString:@" " withString:@""];
[AppDelegate sharedAppDelegate].voipToken = voipToken;
WCLog(@"【MainTabBarController】【voip】Token is %@", voipToken);
}
- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type{
WCLog(@"【MainTabBarController】didReceiveIncomingPushWithPayload<<<<<<<<<<<<11");
[self onRecivepushRegistry:registry didReceiveIncomingPushWithPayload:payload forType:type withCompletionHandler:nil];
}
- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion
{
NSLog(@"【MainTabBarController】didReceiveIncomingPushWithPayload>>>>>>>>>>>>11");
[self onRecivepushRegistry:registry didReceiveIncomingPushWithPayload:payload forType:type withCompletionHandler:completion];
}
- (void)onRecivepushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion
{
//TODO
//處理自己具體的業(yè)務(wù)邏輯
}
這里要重點講講App被kill之后VoIP是怎么喚醒程序的(雙擊home鍵上滑關(guān)閉程序)
一般通知過程
后臺通過deviceToken給APNS服務(wù)器發(fā)通知請求,
然后APNS服務(wù)器再將通知消息發(fā)送到手機上。-
手機收到VoIP消息之后的處理
- 手機會在后臺啟動App,也會走
didFinishLaunchingWithOptions方法 - 然后執(zhí)行與這個方法綁定的業(yè)務(wù)
- 再去尋找注冊注冊pushkit代理的地方
- 后面的實現(xiàn)就是業(yè)務(wù)邏輯側(cè)
整個過程都是在后臺執(zhí)行的,也就是說對用戶來說是完全透明的,包括雙擊home鍵也看不到這個程序的啟動
2.1 用戶在一定時間內(nèi)沒有手動點開App,并且短時間內(nèi)沒有再次受到VoIP消息,則后臺啟動的App會被被系統(tǒng)自動回收
2.2 用戶點開App,則App不會再從didFinishLaunchingWithOptions方法啟動,而是看到的業(yè)務(wù)邏輯側(cè)實現(xiàn)的地方
2.3 短時間內(nèi)又收到了VoIP消息,App也不會從didFinishLaunchingWithOptions方法啟動,而是直接去尋找注冊注冊pushkit代理的地方
- 手機會在后臺啟動App,也會走
這就是為什么VoIP為什么能喚起程序的原因。
結(jié)語:有些坑還是要自己爬過才算真的爬過。
4. 推送服務(wù)端實現(xiàn)
考慮到篇幅長度問題,服務(wù)端的實現(xiàn)可以看這篇
iOS推送服務(wù)端實現(xiàn)
5. 推送工具
pusher工具(還沒弄明白簡書上怎么傳文件的,就上傳到了git上pusher)
只需要選擇推送證書 然后填deviceToken 就可以測試推送了,很好用的一款mac應(yīng)用


如果沒有收到消息,請檢查
- 檢查管理后臺應(yīng)用中是否配置過推送證書。
- 檢查生成證書的環(huán)境是否和管理后臺配置的相同。
- 檢查初始化時填的cername是否和管理后臺配置的一致。
- 打包證書中是否有私鑰。
- provision profile是否未包含新增加的推送證書。
- 代碼調(diào)試是否可以獲取到deviceToken。
參考鏈接 :