iOS推送

前言

現(xiàn)在第三方推送也很多 ,比如極光,融云,信鴿,其原理也是相同利用APNS推送機制 ,公司讓做自己的推送。

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

1. 簡介

1.1 推送機制介紹

首先我們看一下蘋果官方給出的對ios推送機制的解釋。如下圖


推送機制.jpg
  • 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推送通知的詳細工作流程:


詳細工作流程.jpg

根據(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ù)

通知1.png

通知2.png

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 文件中去手動添加

屏幕快照 2017-12-02 16.59.09.png

其實在 Background Modes 勾選上的功能也是要加到 info.plist 中才有效的

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

UIApplicationDelegate.png
這里聲明的,細心的人會發(fā)現(xiàn)這里面有很多代理在新的幾個版本中都標(biāo)紅了,也就是過期了,說明蘋果那邊也想著將通知的實現(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)閉程序)

  1. 一般通知過程
    后臺通過deviceToken給APNS服務(wù)器發(fā)通知請求,
    然后APNS服務(wù)器再將通知消息發(fā)送到手機上。

  2. 手機收到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代理的地方

這就是為什么VoIP為什么能喚起程序的原因。

結(jié)語:有些坑還是要自己爬過才算真的爬過。

4. 推送服務(wù)端實現(xiàn)

考慮到篇幅長度問題,服務(wù)端的實現(xiàn)可以看這篇
iOS推送服務(wù)端實現(xiàn)

5. 推送工具

pusher工具(還沒弄明白簡書上怎么傳文件的,就上傳到了git上pusher
只需要選擇推送證書 然后填deviceToken 就可以測試推送了,很好用的一款mac應(yīng)用

pusher.jpeg

pusher使用說明.png.jpeg

如果沒有收到消息,請檢查

  1. 檢查管理后臺應(yīng)用中是否配置過推送證書。
  2. 檢查生成證書的環(huán)境是否和管理后臺配置的相同。
  3. 檢查初始化時填的cername是否和管理后臺配置的一致。
  4. 打包證書中是否有私鑰。
  5. provision profile是否未包含新增加的推送證書。
  6. 代碼調(diào)試是否可以獲取到deviceToken。

參考鏈接 :

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 注:此文只現(xiàn)在已經(jīng)不能適配iOS10了,iOS10推送采用了新的方法,做iOS9及以下的系統(tǒng)可讀此篇文章。 最近公...
    TIME_for閱讀 33,660評論 85 322
  • 推送通知 注意:這里說的推送通知跟NSNotification有所區(qū)別 NSNotification是抽象的,不可...
    iOS開發(fā)攻城獅閱讀 4,415評論 1 13
  • 小白也能掌握的個推iOS推送集成教程 一次偶然的機會,公司的項目要用到推送,我自己本來就很懶,不愿意去弄整套APN...
    Ezreallp閱讀 1,329評論 0 7
  • 推送: 用戶被動的接收消息,是程序在后臺的一種通知機制推送通知跟NSNotification不同1.NSNotif...
    Reliver閱讀 678評論 0 0
  • 今天約三個閨蜜小聚,照例是喜歡的榴蓮比薩、剔骨牛排、黑森林蛋糕。說的好聽點,我們是對這三種美食“一吃鐘情”,說的難...
    滄浪之水1閱讀 239評論 1 0

友情鏈接更多精彩內(nèi)容