iOS推送

前言

现在第三方推送也很多 ,比如极光,融云,信鸽,其原理也是相同利用APNS推送机制 ,公司让做自己的推送。

  1. 避免device token被第三方泄露,保护手机设备信息;
  2. 第三方部分开始收费,一些免费的以后谁又知道呢 ,不如自己创建,除了后台麻烦点,需要数据库来存储token相关字段(后台配置部分在最后更新添加),前端实现起来并不复杂。
  3. 现有的第三方推送一般都只集成有APNS推送,对于VoIP部分并没有支持,而且信鸽的会导致VoIP消息收不到,网上很多都会说zeropush中有提供VoIP推送,尝试了下,感觉不怎么好集成,所以不如自己做个推送。
  4. 对于iOS8.0以后,原生推送实时性很好,这篇文章参考许多资料将详细的过程讲解它 按照一步一步来很容易实现 。

1. 简介

1.1 推送机制介绍

首先我们看一下苹果官方给出的对ios推送机制的解释。如下图


推送机制.jpg
  • Provider就是我们自己程序的后台服务器
  • APNS是Apple Push Notification Service的缩写,也就是苹果的推送服务器。

上图可以分为三个阶段:

  • 第一阶段:应用程序的服务器端把要发送的消息、目的iPhone的标识打包,发给APNS。
  • 第二阶段:APNS在自身的已注册Push服务的iPhone列表中,查找有相应标识的iPhone,并把消息发送到iPhone。
  • 第三阶段:iPhone把发来的消息传递给相应的应用程序,并且按照设定弹出Push通知。

APNS推送通知的详细工作流程
下面这张图是说明APNS推送通知的详细工作流程:


iOS推送_第1张图片
详细工作流程.jpg

根据图片我们可以概括一下:

  • 应用程序注册APNS消息推送。
  • iOS从APNS Server获取devicetoken,应用程序接收device token。
  • 应用程序将device token发送给程序的PUSH服务端程序。
  • 服务端程序向APNS服务发送消息。
  • APNS服务将消息发送给iPhone应用程序。

1.2 推送类型介绍

本地推送(注册本地推送,比如闹钟)
远程推送(由苹果APNS服务器推送给设备的消息)
  • 普通推送(常见的通知栏消息)
  • 静默推送(可以让App在后台执行一段代码,热修复的原理)
  • VoIP推送(可以直接启动App,然后执行对应的消息)
    • 只有当VoIP发生推送时,设备才会唤醒,从而节省能源。
    • VoIP推送被认为是高优先级通知,并且毫无延迟地传送。
    • VoIP推送可以包括比标准推送通知提供的数据更多的数据。
    • 如果收到VoIP推送时,您的应用程序未运行,则会自动重新启动。
    • 即使您的应用在后台运行,您的应用也会在运行时处理推送。
    • VoIP例子
      • 关闭程序后仍可收到推送
      • 自定义铃声
      • 持续震动
      • 呼叫方挂机后,被叫方马上停止通知铃声,并在通知栏没有留下痕迹

1.3 三方推送平台介绍

  • 信鸽推送(可能会收不到VoIP推送消息,猜测原因与信鸽需要的证书有关)
  • 极光推送
  • 融云推送
  • 网易云推送(不过要集成整个网易云IM)

登录平台的官网都会有详细的集成介绍

1.4 VoIP上架问题

当app要上传App Store时,请在iTunes connect上传页面右下角备注中填写你用到VoIP推送的原因,附加上音视频呼叫用到VoIP推送功能的demo演示链接,演示demo必须提供呼出和呼入功能。

2. 推送证书的制作

考虑到篇幅长度问题,如果对证书制作不熟悉的同学可以看这篇
iOS推送证书制作

3. 推送客户端实现

3.1 APNS推送

xcode -> Capabilities中开启通知服务

通知1.png

iOS推送_第2张图片
通知2.png

3.1.1 注册通知

iOS10以上的需要引入头文件

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
#import 
@interface AppDelegate() 
@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 实现通知代理
  • 注册远程推送
    在注册成功的回调中会返回deviceToken,这一个token是用于APNS通知的的deviceToken,返回的 deviceToken 中包含 <>空格,需要调一下方法去掉这些字符。如果是用的三方推送,则直接调三方推送提供的接口上报 deviceToken 即可,sdk内部会处理这些字符。
/**
 *  注册远程推送
 *
 *  @param application UIApplication 实例
 *  @param deviceToken 设备唯一标识符
 */
- (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
}


/**
 *  注册远程推送失败回调
 *
 *  @param application application description
 *  @param error       注册远程推送失败错误信息
 */
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
    WCLog(@"【APNS】register APNS fail.\n【APNS】 reason : %@", error);
}
  • iOS10之前收到通知的回调
#if __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_10_0
/**
 *  收到通知的回调(iOS10之后废弃)
 *
 *  @param application UIApplication 实例
 *  @param userInfo    推送时指定的参数
 */
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo
{
    
}
#endif
  • iOS10之后收到通知的回调
// iOS 10 新增 API
// iOS 10 会走新 API, iOS 10 以前会走到老 API
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
// App 用户点击通知的回调
// 无论本地推送还是远程推送都会走这个回调
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler
{
    //TODO...   handle notification with type
}

// App 在前台弹通知需要调用这个接口
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
    /**
     *  这个 completionHandle 是回传给 App 的参数
     *
     *  @param UNNotificationPresentationOptionAlert 传了哪个就表示哪儿生效
     *
     *  @return return value
     */
    
    /**
     *  如果是在前台的时候收到通话邀请,则只需要有声音即可
     */
    completionHandler(UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert);
}
#endif
  • 收到静默推送的回调
    除了可以用VoIP启动程序,iOS10之后苹果引入的CallKit框架提供与原生通话相同级别的体验,所以可以在这里使用CallKit框架来调用来电呼入
/**
 *  收到静默推送的回调
 *
 *  @param application  UIApplication 实例
 *  @param userInfo 推送时指定的参数
 *  @param completionHandler 完成回调
 */
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    NSLog(@"[APNS] userinfo %@", userInfo);
    
    //TODO...
    completionHandler(UIBackgroundFetchResultNewData);
}
3.1.3 添加本地通知

这里就直接贴项目中使用的代码,对于通知的实现都放在分类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 
@interface AppDelegate() 
@property (nonatomic, strong) UNNotificationRequest *notifyRequest;
@end
#endif

这里用到的Runtime来实现动态添加属性,不熟的同学可以自己去网上搜搜,很多资源,推荐一篇美团写的博文,很详细链接

#pragma mark - -- 动态添加属性
- (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");
}

发送本地通知,这里用来做测试的通知。有一个要说的,在文章开始介绍VoIP推送的例子中最后一条呼叫方挂机后,被叫方马上停止通知铃声,并在通知栏没有留下痕迹,可以在App将要进入前台的代理中移除对应的通知即可

#pragma mark - -- 推送服务
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
}

移除本地通知,这里移除的都是上个方法中对应的通知,如果是用的测试的Identifiers是移除不了的,下面的这个方法只能移除Identifier == @"Voip_Push"的通知,使用的时候要注意

if (IOS_VERSION_OR_LATER(10)) {
        [[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:@[@"Voip_Push"]];
    } else {
        [[UIApplication sharedApplication] cancelLocalNotification:self.localNotify];
    }

3.2 VoIP推送

这里很重要,总结了一些在开发过程中遇到的坑
老的xcode版本开启VoIP功能也是在 Background Modes 中直接勾选就开启了,但是新版的xcode移除了这个选项,所以只能在 info.plist 文件中去手动添加

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

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

在使用之前,我要说个很重要的问题。就APNS推送给我们的推送开发思维来说,我们习惯将处理推送的代码都放在 AppDelegate 中实现,这个确实没毛病,因为上面实现的那些通知代理都是在

iOS推送_第3张图片
UIApplicationDelegate.png
这里声明的,细心的人会发现这里面有很多代理在新的几个版本中都标红了,也就是过期了,说明苹果那边也想着将通知的实现做的更灵活了。但是,但是,但是... 这个框架可以在程序的任何地方实现,任何你业务逻辑需求的地方,就算不在 Appdelegate 中注册,照样可以通过推送唤醒程序。所以要灵活的使用这个框架,不要被定向开发思维所禁锢。下面来说说 的实现

VoIP实现
  • 引入PushKit库并遵循代理
#import 

@interface MainTabBarController()
@end
  • 初始化pushkit推送服务
#pragma mark - -- 初始化VoIP推送服务
- (void)loadVoipPush
{
    PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
    pushRegistry.delegate = self;
    pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
    [self addApplicationListener];
}
  • 实现pushkit代理
    上面在APNS中也有获取过一个deviceToken,这里系统又返回一个deviceToken,这两个deviceToken是不一样的,发送对应的通知就要使用对应的deviceToken,同样需要上报给服务器,由服务端记录并发送通知。
    对于接受pushkit消息的回调,苹果在iOS11之后做了方法更新,仅仅是加了个处理成功的回调,这个回调我没用到,所以就都集成到同一个方法了。
#pragma mark - -- VoIP推送代理
- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)pushCredentials forType:(PKPushType)type
{
    if ([pushCredentials.token length] == 0) {
        NSLog(@"voip token null");
        return ;
    }
    
    //应用启动获取token,并上传服务器
    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
    //处理自己具体的业务逻辑
}

这里要重点讲讲App被kill之后VoIP是怎么唤醒程序的(双击home键上滑关闭程序)

  1. 一般通知过程
    后台通过deviceToken给APNS服务器发通知请求,
    然后APNS服务器再将通知消息发送到手机上。

  2. 手机收到VoIP消息之后的处理

    • 手机会在后台启动App,也会走didFinishLaunchingWithOptions方法
    • 然后执行与这个方法绑定的业务
    • 再去寻找注册注册pushkit代理的地方
    • 后面的实现就是业务逻辑侧
      整个过程都是在后台执行的,也就是说对用户来说是完全透明的,包括双击home键也看不到这个程序的启动
      2.1 用户在一定时间内没有手动点开App,并且短时间内没有再次受到VoIP消息,则后台启动的App会被被系统自动回收
      2.2 用户点开App,则App不会再从didFinishLaunchingWithOptions方法启动,而是看到的业务逻辑侧实现的地方
      2.3 短时间内又收到了VoIP消息,App也不会从didFinishLaunchingWithOptions方法启动,而是直接去寻找注册注册pushkit代理的地方

这就是为什么VoIP为什么能唤起程序的原因。

结语:有些坑还是要自己爬过才算真的爬过。

4. 推送服务端实现

考虑到篇幅长度问题,服务端的实现可以看这篇
iOS推送服务端实现

5. 推送工具

pusher工具(还没弄明白上怎么传文件的,就上传到了git上pusher)
只需要选择推送证书 然后填deviceToken 就可以测试推送了,很好用的一款mac应用

pusher.jpeg

iOS推送_第4张图片
pusher使用说明.png.jpeg

如果没有收到消息,请检查

  1. 检查管理后台应用中是否配置过推送证书。
  2. 检查生成证书的环境是否和管理后台配置的相同。
  3. 检查初始化时填的cername是否和管理后台配置的一致。
  4. 打包证书中是否有私钥。
  5. provision profile是否未包含新增加的推送证书。
  6. 代码调试是否可以获取到deviceToken。

参考链接 :

  • iOS 远程消息推送 APNS推送原理和一步一步开发详解篇
  • iOS开发证书发布证书,推送证书,描述文件的生成总集
  • iOS利用voip push实现类似微信(QQ)电话连续响铃效果

你可能感兴趣的:(iOS推送)