苹果推送通知服务(APNs)是推送通知的网关,iPhone ipad 对于应用程序在后台运行有诸多限制,考虑到手机电池电量,应用不允许在后台进行过多的操作。因此,当用户切换到其他程序后,原先的程序无法保持运行状态。对于那些需要保持持续连接状态的应用程序(比如社区网络应用),将不能收到实时的信息。推送是解决轮询所造成的流量消耗和电量消耗的一个比较好的解决方案
为解决这一限制,苹果推出了APNs(苹果推送通知服务 Apple Push Notification services)。APNs 允许设备与苹果的推送通知服务器保持常连接状态。当你想发送一个推送通知给某个用户的iPhone上的应用程序时,你可以使用 APNs 发送一个推送消息给目标设备上已安装的某个应用程序。
苹果的推送服务APNs基本原理简单来说就是苹果利用自己专门的推送服务器(APNs)接收来自我们自己应用服务器的需要被推送的信息,然后推送到指定的iOS设备上,然后由设备通知到我们的应用程序,设备以通知或者声音的形式通知用户有新的消息。推送的前提是装有我们应用的设备需要向APNs服务器注册,注册成功后APNs服务器会返给我们一个device_token,拿到这个token后我们将这个token发给我们自己的应用服务器,当有需要被推送的消息时,我们的应用服务器会将消息按指定的格式打包,然后结合设备的device_token一并发给APNs服务器,由于我们的应用和APNs维持一个基于TCP的长连接,APNs将新消息推送到我们设备上,然后在屏幕上显示出新消息来。
我们的设备和APNS服务器之间的通讯是基于SSL协议的TCP流通讯,二者之间维持一个长连接。
上图显示的这个消息体就是我们的服务器(Provider)发送给APNS服务器的消息结构,APNS验证这个结构正确并提取其中的信息后,再将消息推送到指定的设备。
//Payload内容
{
"aps":{
"alert":"一些文字",
"badge":"1",
"sound":"default",
}
}
这其实就是个JSON结构体,alert标签的内容就是会显示在用户手机上的推送信息,badge显示的数量(注意是整型)是会在应用Icon右上角显示的数量,提示有多少条未读消息等,sound就是当推送信息送达是手机播放的声音,传defalut就标明使用系统默认声音,如果传比如“beep.wav”就会播放在我们应用工程目录下名称为beep.wav的音频文件,比如当手机锁屏时QQ在后台收到新消息时的滴滴声。
有这么一种情况,当我们将应用从设备卸载后,推送的消息改如何处理呢。我们知道,当我们将应用从设备卸载后,我们是收不到Provider给我们推送的消息的,但是,如何让APNS和Provider都知道不去向这台卸载了应用的设备推送消息呢?针对这个问题,苹果也已经帮我们解决了,那就是Feedback service。他是APNS的一部分,APNS会持续的更新Feedback service的列表,当我们的Provider将信息发给APNS推送到我们的设备时,如果这时设备无法将消息推送到指定的应用,就会向APNS服务器报告一个反馈信息,而这个信息就记录在feedback service中。按照这种方式,Provider应该定时的去检测Feedback service的列表,然后删除在自己数据库中记录的存在于反馈列表中的device_token,从而不再向这些设备发送推送信息。连接Feedback service的过程同样使用Socket的方式,连接上后,直接接收由APNS传输给我们的反馈列表,传输完成后断开连接,然后我们根据这个最新的反馈列表在更新我们自己的数据库,删除那些不再需要推送信息的设备的device_token。从Feedback service读取的数据结构如下:
结构中包含三个部分,第一部分是一个时间戳,记录的是设备失效后的时间信息,第二个部分是device_token的长度,第三部分就是失效的device_token,我们所要获取的就是第三部分,跟我们的数据库进行对比后,删除对应的device_token,下次不再向这些设备发送推送信息。
首先要有一台苹果的设备,模拟器是不支持推送的,所以你需要一台iphone,ipod touch或者ipad。
准备CSR文件,生成带有Push Notifications功能的AppID,给该AppID的Push Notifications配置CSR,下载证书配置钥匙串,导出p12文件。
其中
证书: 内容是公钥或私钥,由其他机构对其签名组成的数据包。
Entitlements: 包含了 App 权限开关列表。
CertificateSigningRequest: 本地公钥。
p12: 本地私钥,可以导入到其他电脑。
Provisioning Profile: 包含了 证书 / Entitlements 等数据,并由苹果后台私钥签名的数据包。
这样,全部配置方面的事情就做完了。
iOS处理生命周期和推送都在AppDelegate里面,AppDelegate继承自UIResponder,遵守UIApplicationDelegate协议,其中跟推送相关的方法都在UIApplicationDelegate协议中。包含本地推送和远程推送。
由于iOS不断更新,推送相关的配置方法也不断变化,iOS8,iOS 10分别更新了一些方法,废弃了一系列方法。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self registPushNotification:application];
//程序未启动时,收到远程推送,保存下来,在应用进入前台后可以处理。
_launchNotification = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
}
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings
{
[application registerForRemoteNotifications];
}
- (void)registPushNotification:(UIApplication *)application
{
if ([application respondsToSelector:@selector(registerForRemoteNotifications)]) { // ios8+的远程通知注册
UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert;
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];
[application registerForRemoteNotifications];
}
else { // ios8以下
SUPPRESS_DEPRECATED_WARNING([application registerForRemoteNotificationTypes:(UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)]);
}
}
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo{
[self processLocalNotification:userInfo];
}
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{
}
上面的方法,可以让推送达到正常使用的水平。
推送处理流程
处理推送必须是通过点击通知进入,收到通知,点应用进入不会触发通知处理的方法。
可以看到程序在启动的情况下,不管在前台还是后台,都会进入到- (void)application:(UIApplication )application didReceiveRemoteNotification:(NSDictionary )userInfo方法中,区别只是系统会不会进行通知栏弹窗,如果想要前台弹窗,就需要自己去做效果了。
另外可以通过application.applicationState != UIApplicationStateActive来判断当前app前后台的状态,进而分别处理。
iOS 10推荐的推送处理方式:
优点:
引入
#import
使用UNUserNotificationCenterDelegate协议里的方法,
具体代码中:
- (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions{
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
// 必须写代理,不然无法监听通知的接收与点击
center.delegate = self;
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound) completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted) {
// 点击允许
NSLog(@"注册成功");
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
NSLog(@"%@", settings);
}];
} else {
// 点击不允许
NSLog(@"注册失败");
}
}];
}
// iOS 10收到通知
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler{
NSDictionary * userInfo = notification.request.content.userInfo;
UNNotificationRequest *request = notification.request; // 收到推送的请求
UNNotificationContent *content = request.content; // 收到推送的消息内容
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]]) {
NSLog(@"iOS10 前台收到远程通知:%@", [self logDic:userInfo]);
}
else {
// 判断为本地通知
NSLog(@"iOS10 前台收到本地通知:{\\\\nbody:%@,\\\\ntitle:%@,\\\\nsubtitle:%@,\\\\nbadge:%@,\\\\nsound:%@,\\\\nuserInfo:%@\\\\n}",body,title,subtitle,badge,sound,userInfo);
}
completionHandler(UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionSound|UNNotificationPresentationOptionAlert); // 需要执行这个方法,选择是否提醒用户,有Badge、Sound、Alert三种类型可以设置,Alert可以设定前台展示通知栏。
}