iOS推送分为远程推送 和 本地推送两种,本地推送此文章先不说,之后会有新的文章更新。
远程推送将一些重要的信息推送到用户的相关APP上,不管你的APP是否运行,或者在后台挂起 或者 完全杀死,都能收到推送。并且在iOS10 之后,苹果允许在收到推送内容后有30s 的时间对推送的内容进行修改,或者下载一些图片(这个在文章的后面会提到),推送会展示一个弹框(alert),声音(sound),APP icon 上的红色标志(badge)。
远程推送的原理
苹果推送服务通知是由自己专门的推送服务器APNS(Apple push Notification service)来完成的,其过程是apns 接收到我们自己的应用服务器发出的被推送消息,将这条消息推送到指定的iOS设备上,然后再由iOS设备通知到我们的应用程序,我们将会以通知或者声音的形式受到推送回来的消息。
我们的APP在启动的时候,会携带设备号和应用id 想APNs 注册推送服务,成功后APNs 会返回一个标识devicetoken,然后我们会将收到的devicetoken发送到我们自己的服务器,当我们需要推送消息时,我们的服务器会将推送内容payLoad(之前是不超过256字节,现在应该比较宽松了,具体未定,json 格式)和devicetoken发送给APNs服务器。APNs服务器将新消息推送到iOS设备上(在有网的情况下设备会与APNs服务器建立一个长连接tcp)。
devicetoken 在以下情况下会发生改变:
1、同一款设备上重新安装同一款应用
2、不同设备上安装同一款应用
3、设备重新升级了系统,同一个APP对应的devicetoken也会发生改变
-
APP注册推送服务过程
上述图片完成了如下流程:
1、安装了推送功能APP的设备,携带者设备号和APPid 连接APNs服务器。
2、连接成功后,APNs 通过打包和加密等处理生成devicetoken,返回给注册的设备。
3、APP拿到devicetoken后,将它发送给我们自己的服务器。
4、完成需要被推送的设备在苹果服务器和我们自己服务器之前的注册。
-
推送过程
上述图片完成如下流程:
1、首先,我们的设备安装了具有推送功能的APP后(APP在启动的时候会在lunch方法里注册推送),在有网络的情况下会连接到apns 推送服务器,在连接的过程中apns 会解密设备的devicetoken进行验证,验证成功后,建立tcp 连接。
2、Provider(我们自己的应用服务器)将 要被推送的消息结合接收消息的iOS设备的devicetoken发送给apns服务器
3、apns 收到Provider 发送过来的推送消息,解密devicetoken进行验证,验证成功后将消息发送给指定的设备
4、iOS设备收到苹果推送的消息后,通知我们的APP并显示和提示用户(alert,sound,badge)。
比较直观的流程图:
信息结构图:
上图显示的这个消息体就是Provider(我们服务器)发送给apns服务器的消息结构,APNs 验证这个结构正确并提取其中的信息后,再将消息推送给指定的iOS设备。这个结构体包括五个部分,第一个部分是命令标识符,第二个部分是devicetoken的长度,第三部分是devicetoken字符串,第四部分是推送消息体(payload)的长度,最后一部分也就是真正的消息内容了,里面包含了推送的基本信息,比如消息内容,应用icon右上角显示多少数字以及推送消息到达时所播放的声音等。
payload的结构:
{
“aps”:{
“alert”:“CSDN给您发送了新消息”,
“badge”:1,
“sound”:“default”
},
}
这其实就是个json结构体,alert标签的内容就是会显示在用户手机上的推送信息,badge 显示的数量是会在APP icon右上角显示的数量,提示有多少条未读消息等,sound就是当推送信息送达时手机播放的声音,没有特殊要求就是default 系统默认声音。
使用自己服务器完成推送
1、iOS端代码
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
/*
注册推送服务
申请APP需要接受来自服务商提供推送消息
launchOptions:保存了app启动的原因信息,如果app是因为点击通知栏启动的,可以在launchOptions获取到通知的具体内容
*/
//判断是不是点击通知栏启动
NSDictionary *remoteNotification = [launchOptions objectForKey:@"UIApplicationLaunchOptionsRemoteNotificationKey"];
if (remoteNotification != nil) {
// 点击通知栏消息启动
self.isLaunchedByNotification = YES;
}else{
// 不是点击通知栏消息启动
self.isLaunchedByNotification = NO;
}
//iOS10以后的注册方法
if ([[UIDevice currentDevice].systemVersion floatValue] >= 10.0) {
//来自UserNotification框架
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
// center.delegate = self;
// 请求授权
[center requestAuthorizationWithOptions:UNAuthorizationOptionBadge |UNAuthorizationOptionSound | UNAuthorizationOptionAlert completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted) {//授权成功
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
NSLog(@"=======%@", settings);
}];
} else {
//点击不允许,注册失败
}
}];
}
} else if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {
[[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert categories:nil]];
} else {
[[UIApplication sharedApplication]registerForRemoteNotificationTypes:UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert];
}
//最后一定要调用这个方法,不然收不到apns返回的devicetoken
[[UIApplication sharedApplication]registerForRemoteNotifications];
发起申请后 会有两个回调方法,一个是注册成功,返回devicetoken,另一个是注册失败的回调方法
//成功
- (void)application:(UIApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
NSLog(@"deviceToken = %@",deviceToken);
//我们会在收到此方法后,将收到的devicetoken 发送给我们自己的服务器,我们的服务器在将消息发送给apns 时会用到,用于apns 的验证和识别
}
//注册apns失败回调
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
//Optional
HLLog(@"did Fail To Register For Remote Notifications With Error: %@", error);
}
以上是我们本地代码完成了在apns 和 Provider 之间的注册,以下的方法是在apns 成功推送到我们设备后的回调方法。
//iOS10 在前台收到通知
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
UNNotificationRequest *request = notification.request; //收到推送的请求
UNNotificationContent *content = request.content;//收到推送的消息内容
NSDictionary *userInfo = content.userInfo;
NSNumber *badge = content.badge;//推送的角标
NSString *body = content.body;//推送消息体
UNNotificationSound *sound = content.sound;//声音
NSString *subtitle = content.subtitle;//推送消息的副标题
NSString *title = content.title;//推送消息的标题
if ([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
//UNPushNotificationTrigger 触发器,专门用于远程推送,其他一般是本地通知要用到的
} else {
//本地通知
}
UNNotificationPresentationOptions options = UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert;
completionHandler(options);
}
//ios10 点击推送消息
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
UNNotificationRequest *request = response.notification.request;
UNNotificationContent *content = request.content;
NSDictionary *userInfo = content.userInfo;
NSNumber *badge = content.badge;
NSString *body = content.body;
NSString *subtitle = content.subtitle;
NSString *title = content.title;
if ([request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
//远程推送
} else {
//本地通知
}
completionHandler();
}
/*
iOS7及以上,推送字段必须包含content-available = 1 并且Background Modes 中勾选Remote notifications,才能调用此方法.
如果满足上述条件,那么收到推送消息时,应用在前台和后台不杀死的情况下还有点击通知栏消息都会调用此方法,可以在此方法内做一些后台操作,如下载数据 更新UI等
*/
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
NSDictionary *aps = userInfo[@"aps"];
NSString * storeid = aps[@"order_id"];
if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
}else if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground){
HLLog(@"程序在后台 或从后台点击通知栏 运行 该方法");
}else{
HLLog(@"后台处于前台的过度");
}
completionHandler(UIBackgroundFetchResultNewData);
}
现在看一下服务端的代码,大致了解一下,这段也是摘自网上的相关文章的。
在此之前我们是需要将推送证书导出成p12文件交给服务器端,不会导出的自行百度。
java端代码
import javapns.back.PushNotificationManager;
import javapns.back.SSLConnectionHelper;
import javapns.data.Device;
import javapns.data.PayLoad;
public class pushService {
public static void main(String[] args) {
try {
//这个token 就是客户端从APNs服务器获取到的devicetoken
String deviceToken = "eab6df47eb4f81e0aaa93bb208cffd7dc3884fd346ea0743fcf93288018cfcb6";
//被推送的iphone应用程序标示符
PayLoad payLoad = new PayLoad();
payLoad.addAlert("测试我的push消息");
payLoad.addBadge(1);
payLoad.addSound("default");
PushNotificationManager pushManager = PushNotificationManager.getInstance();
pushManager.addDevice("iphone", deviceToken);
//测试推送服务器地址:gateway.sandbox.push.apple.com /2195
//产品推送服务器地址:gateway.push.apple.com / 2195
String host="gateway.sandbox.push.apple.com"; //测试用的苹果推送服务器
int port = 2195;
String certificatePath = "/Users/hsw/Desktop/PushTest/PushTest.p12"; //刚才在mac系统下导出的证书
String certificatePassword= "123456";
pushManager.initializeConnection(host, port, certificatePath,certificatePassword, SSLConnectionHelper.KEYSTORE_TYPE_PKCS12);
//Send Push
Device client = pushManager.getDevice("iphone");
pushManager.sendNotification(client, payLoad); //推送消息
pushManager.stopConnection();
pushManager.removeDevice("iphone");
}
catch (Exception e) {
e.printStackTrace();
System.out.println("push faild!");
return;
}
System.out.println("push succeed!");
}
}
远程推送涉及到的方法
1、 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
2、 - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo;
3、 - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler;
4、 - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler;
5、 - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler;
1、会在APP启动完成后调用,lunchOptions保存了app启动的原因信息,如果app是因为点击通知栏启动的,可以在lunchOptions 获取到通知的具体内容(上文已经提到如何获取)
2、会在接收到通知的时候调用,在最新的iOS10中已经废弃,建议不再使用。
3、在iOS7之后新增的方法,可以说是2的升级版本,如果APP最低支持iOS7的话可以不用添加2,其中completionHandler这个block可以填写的参数UIBackgroundFetchResult是一个枚举值。主要是用来在后台状态下进行一些操作的,比如请求数据,操作完成之后,必须通知系统获取完成,可供选择的结果:
typedef NS_ENUM(NSUInteger, UIBackgroundFetchResult) {
// 获取到了新数据(此时系统将对现在的UI状态截图并更新APP Switcher中你的应用截屏)
UIBackgroundFetchResultNewData,
UIBackgroundFetchResultNoData,//没有新数据
UIBackgroundFetchResultFailed//获取失败
}
以上操作的前提是已经在Background Modes 里面勾选了Remote notifications(推送唤醒)且推送的消息中包含content-available = 1字段。
4、是iOS10新增的 UNUserNotificationCenterDelegate 代理方法,在ios10的环境下,点击通知栏都会调用这个方法。
5、也是iOS10新增的代理方法,在iOS10 以前,如果应用处于前台状态,接收到推送,通知栏是不会有任何提示的,如果开发者需要展示通知,需要在3方法中提取到通知内容展示。在iOS10 中,如果开发者需要前台展示通知,可以再在这个方法completionHandler传入相应的参数。
typedef NS_OPTIONS(NSUInteger, UNNotificationPresentationOptions) {
UNNotificationPresentationOptionBadge = (1 << 0),
UNNotificationPresentationOptionSound = (1 << 1),
UNNotificationPresentationOptionAlert = (1 << 2),
}
- 当程序处于关闭状态的时候收到推送消息,点击应用程序图标无法获取推送消息,iOS10环境下,点击通知栏会调用1,4,非iOS10的情况下 会调用1,3
- 当程序处于前台状态下收到推送消息,iOS10的环境下如果推送的消息包含content-available字段的话,执行方法3,5,否则只执行5,非iOS10的情况会执行3
- 当程序处于后台收到推送消息,如果已经在Background Modes 里面勾选了Remote notifications(推送唤醒)且推送消息中包含content-available 字段的话,都会执行3,点击通知栏iOS10 执行4,非iOS执行3.