iOS远程推送
一.什么是远程通知
概念:由服务器发送消息给用户弹出消息的通知(需要联网)
远程推送服务,又称为APNs(Apple Push Notification Services)
二.为什么需要远程通知
- 例子:淘宝最近双11搞活动,各种送红包,想告知用户.但是该用户不经常打包淘宝APP.淘宝如何通知该用户有最新的活动呢?
- 传统方式:只有用户打开了淘宝客户端,客户端向服务器请求是否有最新的活动,才能在APP中告知用户活动.
- 局限性:只要用户关闭了app,就无法跟app的服务器沟通,无法从服务器上获得最新的数据内容
- 远程通知的好处:不管用户打开还是关闭app,只要联网了,都能接收到服务器推送的远程通知
三.远程通知的原理
iOS app大多数都是基于client/server模式开发的,client就是安装在我们设备上的app,server就是远程服务器,主要给我们的app提供数据,因为也被称为Provider。那么问题来了,当App处于Terminate状态的时候,当client与server断开的时候,client如何与server进行通信呢?是的,这时候Remote Notifications很好的解决了这个困境。苹果所提供的一套服务称之为Apple Push Notification service,就是我们所谓的APNs。
推送消息传输路径: Provider-APNs-Client App
Paste_Image.png Paste_Image.png 远程推送原理我们的设备联网时(无论是蜂窝联网还是Wi-Fi联网)都会与苹果的APNs服务器建立一个长连接(persistent IP connection),当Provider推送一条通知的时候,这条通知并不是直接推送给了我们的设备,而是先推送到苹果的APNs服务器上面,而苹果的APNs服务器再通过与设备建立的长连接进而把通知推送到我们的设备上(参考图1-1,图1-2)。而当设备处于非联网状态的时候,APNs服务器会保留Provider所推送的最后一条通知,当设备转换为连网状态时,APNs则把其保留的最后一条通知推送给我们的设备;如果设备长时间处于非联网状态下,那么APNs服务器为其保存的最后一条通知也会丢失。Remote Notification必须要求设备连网状态下才能收到,并且太频繁的接收远程推送通知对设备的电池寿命是有一定的影响的。
#######3.1 为什么淘宝服务器不直接推消息给用户?
- 在通常情况下服务器端是不能主动向客户端推消息的.
- 如果想服务器端给客户端推消息,必须建立长连接
- 淘宝客户端在处于后台时不能和服务器端建立长连接
#######3.2 为什么苹果服务器可以推消息给用户?
所有的苹果设备,在联网状态下,都会与苹果的服务器建立长连接
- 苹果建立长连接的作用:
- 时间校准
- 系统升级提示
- 查找我的iPhone
- 远程通知
#######3.3 疑惑:苹果在推送消息时,如何准确的推送给某一个用户,并且知道是哪一个APP?
-
在淘宝服务器把消息给苹果的APNs服务器时,必须告知苹果DeviceToken
-
什么是DeviceToken?
- deviceToken其实就是根据注册远程通知的时候向APNs服务器发送的Token key,Token key中包含了设备的UDID和App的Bundle Identifier,然后苹果APNs服务器根据此Token key编码生成一个deviceToken。deviceToken可以简单理解为就是包含了设备信息和应用信息的一串编码。
- 通过DeviceToken可以找到唯一手机中的唯一应用程序
-
有什么用:上面提到Provider推送消息的时候必须带有此deviceToken,然后此消息就根据deviceToken(UDID + App's Bundle Identifier)找到对应的设备以及该设备上对应的应用,从而把此推送消息推送给此应用。
-
唯一性:苹果APNs的编码技术和deviceToken的独特作用保证了他的唯一性。唯一性并不是说一台设备上的一个应用程序永远只有一个deviceToken,当用户升级系统的时候deviceToken是会变化的。
-
如何获得DeviceToken?
图1-3.png 图1-4.png 获取DeviceToken当一个App注册接收远程通知时,系统会发送请求到APNs服务器,APNs服务器收到此请求会根据请求所带的key值生成一个独一无二的value值也就是所谓的deviceToken,而后APNs服务器会把此deviceToken包装成一个NSData对象发送到对应请求的App上。然后App把此deviceToken发送给我们自己的服务器,就是所谓的Provider。Provider收到deviceToken以后进行储存等相关处理,以后Provider给我们的设备推送通知的时候,必须包含此deviceToken。(参考图1-3,图1-4)
四.如何做远程通知
- 1, 首先BundleID对应的APPID必须是明确的(特殊功能)
- 2, 该APPID必须配置两个证书
- 开发证书:用于调试远程推送
- 发布证书:用于发布后给用户推送消息
- 3, 根据上面的APPID重新配置描述文件
- 4, 安装对应的证书,即可开始测试远程推送
五: 远程通知证书配置
#######5.1 .配置一个明确的APPID
-
1, 选择明确的APPID,并且将远程通知功能选中
APPID.png
-
2, 显示Push Notifications并非Enabled,而是Configurable.
需要配置对应的证书
Push Notifications.png
#######5.2 证书的配置
########5.2.1 : 在Certificates中配置证书
-
1,选择证书的类型(调试和发布都需要配置)
配置证书.png -
2, 选择为哪一个APPID配置证书
哪一个APPID配置证书.png -
3, 其他步骤同真机调试和发布程序
-
4, 配置完成后获得两个证书文件
########5.2.2配置描述文件
和真机描述文件完全一致
配置描述文件.png
六: 获取DeviceToken
6.1 在苹果的APNs服务器注册,以获取DeviceToken
通常在didFinishLaunchingWithOptions
中添加如下代码进行注册
if ([UIDevice currentDevice].systemVersion.doubleValue >= 8.0) {
// 1.向用户请求可以给用户推送消息
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert categories:nil];
[application registerUserNotificationSettings:settings];
// 2.注册远程通知(拿到用户的DeviceToken)
[application registerForRemoteNotifications];
} else {
[application registerForRemoteNotificationTypes:UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound];
}
6.2 注册之后在另外一个代理方法中,拿到DeviceToken
// 注册成功回调方法,其中deviceToken即为APNs返回的token
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
[self sendProviderDeviceToken:deviceToken]; // 将此deviceToken发送给Provider
}
// 注册失败回调方法,处理失败情况
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
}
在iOS8之后增加了可操作通知类型,可操作通知允许开发者添加自定义跳转事件。这些高级功能此篇文章不讲解,有兴趣的同学可自己去了解UIUserNotificationAction
,UIMutableUserNotificationAction
,UIUserNotificationCategory
,UIMutableUserNotificationCategory
这几个类。
6.3 将DeviceToken发送到服务器即可
6.4 处理接收到远程通知消息(会回调以下方法中的某一个)
- 1,
application: didFinishLaunchingWithOptions:
此方法在程序第一次启动是调用,也就是说App从Terminate
状态进入Foreground
状态的时候,根据方法内代码判断是否有推送消息。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// userInfo为收到远程通知的内容
NSDictionary *userInfo = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
if (userInfo) {
// 有推送的消息,处理推送的消息
}
return YES;
}
- 2,
application: didReceiveRemoteNotification:
如果App处于Background状态时,只用用户点击了通知消息时才会调用该方法;如果App处于Foreground状态,会直接调用该方法。
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
}
- 3,
application: didReceiveRemoteNotification: fetchCompletionHandler:
OS7之前苹果是不支持多任务的,这也是iOS系统对硬件要求低,流畅性好的原因之一。iOS7之后,苹果开始支持多任务,即App可在后台做一些更新UI、下载数据的操作等。若要接收到远程推送的时候要在后台做一些事情则需要把后台远程推送模式打开。不适配iOS7之前系统的项目建议使用此后台模式,充分利用苹果推出的多任务模式,不枉费苹果的一片苦心啊!设置后台模式方法项目对应TARGETS-Capabilities-Background Modes-Remote Notifications具体设置方法如下图(图2-1)。
图2-1
此方法不论App处于Foreground状态还是处于Background状态,收到远程推送消息的时候都会立即调用此方法。此方法需要配置后台模式并且在推送负载中必须有content-available此key值,对应的value值为1(详细介绍参考下面【远程通知负载内容】)。
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
// 在此方法中一定要调用completionHandler这个回调,告诉系统是否处理成功
UIBackgroundFetchResultNewData, // 成功接收到数据
UIBackgroundFetchResultNoData, // 没有接收到数据
UIBackgroundFetchResultFailed // 接受失败
if (userInfo) {
completionHandler(UIBackgroundFetchResultNewData);
} else {
completionHandler(UIBackgroundFetchResultNoData);
}
}
可操作通知类型收到推送消息时回调方法
// 此两个回调方法对应可操作通知类型,具体使用方法参考以上方法很容易理解,不在详细叙述
- (void)application:(UIApplication *)application handleActionWithIdentifier:(nullable NSString *)identifier
forRemoteNotification:(NSDictionary *)userInfo
completionHandler:(void(^)())completionHandler {
}
- (void)application:(UIApplication *)application handleActionWithIdentifier:(nullable NSString *)identifier
forRemoteNotification:(NSDictionary *)userInfo withResponseInfo:(NSDictionary *)responseInfo
completionHandler:(void(^)())completionHandler {
}
7. 测试远程通知
-
当前我们没有自己的服务器,如何测试?
-
可以使用一个第三方的Mac程序来测试:PushMeBaby
-
使用该程序需要修改一些内容
-
1, 编译程序,报错的行注释掉
- 2, 将调试的cer证书,拖入该项目的mainBundle中,并且修改名字为apn.cer
- 3, 运动pushMeBaby程序
- 4, 注意:填写的内容
- 填写推送给的DeviceToken
- 添加推送的内容:固定格式
- {"aps":{"alert":"弹出的信息","badge":1,"sound":"声音","info":"额外信息"}}
八: 监听远程通知的点击
8.1 为什么要监听远程通知的点击
- 比如有需求:点击通知之后跳转到某一个界面中
- 微信/QQ等应用并没有做该功能(不常见)
8.2 如何监听点击
-
1, 应用程序分很多种状态
- 在前台: 如果在前台不需要进行页面跳转
- 在后台: 点击应用时进行页面的跳转
- 被杀死: 点击应用打开应用时,进行页面的跳转
-
2, 当应用程序出于后台时的监听
// 和本地通知基本一致,只是这里是接收到远程通知
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
// 跳转到固定的界面
if (application.applicationState == UIApplicationStateInactive) {
// 进行页面的跳转
} else {
// 其他情况不需要跳转
}
}
- 3, 应用程序被杀死时的监听
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 判断是否是通过点击通知打开了应用程序
if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) {
// 跳转代码
}
return YES;
}
九: 指定用户的推送
对于要求用户登录的App,推送是可以指定用户的,同一条推送有些用户可以收到,但是有些用户又不能收到。说起来这个就要提到另外的一个token了,一般称之为userToken,userToken一般都是根据自己公司自定义的规则去生成的。userToken是以用户的账号加对应的密码生成的。这样结合上面提到的deviceToken,就可以做到根据不同的用户推送不同的消息。deviceToken找到对应某台设备和该设备上的应用,而userToken对应找到该用户。客户端在上报deviceToken的时候,要把userToken对应一起上报给服务端也就是Provider。
十: 激光推送的使用
10.1 激光推送的作用
- 激光推送的作用非常简单,就是将我们服务器需要做的事情用激光推送服务器作为替代
- 流程图
10.2 如何集成激光推送?
- 1,下载激光推送的SDK
- 百度搜索激光推送,进入官网
- 点击官网上方的文档
- 2, 点击iOS SDK集成指南
- 3.文档中集成过程非常非常详细,此处不再累述
- 4.测试集成是否成功
十一: 利用runtime实现推送消息万能跳转
此段参考了@汉斯哈哈哈的一篇iOS 万能跳转界面方法万能跳转就是可以跳转到指定的任意一个界面,但是这个和服务端耦合性太强,使用的时候要慎重考虑,而且公司一般都是iOS,Android共用同一套推送规则很难让服务端在给你开一条新的推送规则,不便于维护,而且成本也是需要考虑的。写此段的目的就是当产品有这样的需求的时候还是可以参考一下的。
#######11.1 定义推送规则
// 客户端控制器的属性
@interface YBViewController : UIViewController
/** 频道Id */
@property (nonatomic, copy) NSString *Id;
/** 频道type */
@property (nonatomic, copy) NSString *type;
@end
// 服务端推送数据格式
{
"aps" : { "alert" : "Provider push messag" },
"class" : "YBViewController",
"property" : {
"Id" : 1314,
"type" : "customType"
}
}
########11.2 跳转逻辑
/ 接收到推送后跳转
- (void)didReceiveRemoteNotificationAndPushToViewController:(NSDictionary *)userInfo {
// 创建类
NSString *class = userInfo[@"class"];
const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];
Class newClass = objc_getClass(className);
if (!newClass) {
Class superClass = [NSObject class];
newClass = objc_allocateClassPair(superClass, className, 0);
objc_registerClassPair(newClass);
}
// 创建跳转控制器对象
id destinationViewController = [[newClass alloc] init];
// 对该对象赋值属性
NSDictionary *propertys = userInfo[@"property"];
[propertys enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
// 检测这个对象是否存在该属性
if ([self checkIsExitPropertyWithdestinationViewController:destinationViewController verifyPropertyName:key]) {
[destinationViewController setValue:obj forKey:key];
}
}];
// 跳转
UITabBarController *tabViewController = (UITabBarController *)self.window.rootViewController;
UINavigationController *sourceViewController = (UINavigationController *)tabViewController.viewControllers[tabViewController.selectedIndex];
[sourceViewController pushViewController:destinationViewController animated:YES];
}
// 检测对象是否存在该属性
- (BOOL)checkIsExitPropertyWithdestinationViewController:(id)destinationViewController verifyPropertyName:(NSString *)verifyPropertyName {
// 获取对象里的属性列表
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList([destinationViewController class], &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
// 属性名转成字符串
NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
// 判断该属性是否存在
if ([propertyName isEqualToString:verifyPropertyName]) {
free(properties);
return YES;
}
}
free(properties);
return NO;
}
参考:
苹果开发者文档Local and Remote Notification Programming Guide
iOS 万能跳转界面方法
iOS推送之远程推送