前言
现在第三方推送也很多 ,比如极光,融云,信鸽,其原理也是相同利用APNS推送机制 ,公司让做自己的推送。
- 避免device token被第三方泄露,保护手机设备信息;
- 第三方部分开始收费,一些免费的以后谁又知道呢 ,不如自己创建,除了后台麻烦点,需要数据库来存储token相关字段(后台配置部分在最后更新添加),前端实现起来并不复杂。
- 现有的第三方推送一般都只集成有APNS推送,对于VoIP部分并没有支持,而且信鸽的会导致VoIP消息收不到,网上很多都会说zeropush中有提供VoIP推送,尝试了下,感觉不怎么好集成,所以不如自己做个推送。
- 对于iOS8.0以后,原生推送实时性很好,这篇文章参考许多资料将详细的过程讲解它 按照一步一步来很容易实现 。
1. 简介
1.1 推送机制介绍
首先我们看一下苹果官方给出的对ios推送机制的解释。如下图
- Provider就是我们自己程序的后台服务器
- APNS是Apple Push Notification Service的缩写,也就是苹果的推送服务器。
上图可以分为三个阶段:
- 第一阶段:应用程序的服务器端把要发送的消息、目的iPhone的标识打包,发给APNS。
- 第二阶段:APNS在自身的已注册Push服务的iPhone列表中,查找有相应标识的iPhone,并把消息发送到iPhone。
- 第三阶段:iPhone把发来的消息传递给相应的应用程序,并且按照设定弹出Push通知。
APNS推送通知的详细工作流程
下面这张图是说明APNS推送通知的详细工作流程:
根据图片我们可以概括一下:
- 应用程序注册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
中开启通知服务
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
文件中去手动添加
其实在
Background Modes
勾选上的功能也是要加到
info.plist
中才有效的
在使用
之前,我要说个很重要的问题。就APNS推送给我们的推送开发思维来说,我们习惯将处理推送的代码都放在 AppDelegate
中实现,这个确实没毛病,因为上面实现的那些通知代理都是在
这个框架可以在程序的任何地方实现,任何你业务逻辑需求的地方,就算不在
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键上滑关闭程序)
一般通知过程
后台通过deviceToken给APNS服务器发通知请求,
然后APNS服务器再将通知消息发送到手机上。-
手机收到VoIP消息之后的处理
- 手机会在后台启动App,也会走
didFinishLaunchingWithOptions
方法 - 然后执行与这个方法绑定的业务
- 再去寻找注册注册pushkit代理的地方
- 后面的实现就是业务逻辑侧
整个过程都是在后台执行的,也就是说对用户来说是完全透明的,包括双击home键也看不到这个程序的启动
2.1 用户在一定时间内没有手动点开App,并且短时间内没有再次受到VoIP消息,则后台启动的App会被被系统自动回收
2.2 用户点开App,则App不会再从didFinishLaunchingWithOptions
方法启动,而是看到的业务逻辑侧实现的地方
2.3 短时间内又收到了VoIP消息,App也不会从didFinishLaunchingWithOptions
方法启动,而是直接去寻找注册注册pushkit代理的地方
- 手机会在后台启动App,也会走
这就是为什么VoIP为什么能唤起程序的原因。
结语:有些坑还是要自己爬过才算真的爬过。
4. 推送服务端实现
考虑到篇幅长度问题,服务端的实现可以看这篇
iOS推送服务端实现
5. 推送工具
pusher工具(还没弄明白上怎么传文件的,就上传到了git上pusher)
只需要选择推送证书 然后填deviceToken 就可以测试推送了,很好用的一款mac应用
如果没有收到消息,请检查
- 检查管理后台应用中是否配置过推送证书。
- 检查生成证书的环境是否和管理后台配置的相同。
- 检查初始化时填的cername是否和管理后台配置的一致。
- 打包证书中是否有私钥。
- provision profile是否未包含新增加的推送证书。
- 代码调试是否可以获取到deviceToken。
参考链接 :
- iOS 远程消息推送 APNS推送原理和一步一步开发详解篇
- iOS开发证书发布证书,推送证书,描述文件的生成总集
- iOS利用voip push实现类似微信(QQ)电话连续响铃效果