iOS远程推送

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注册推送服务过程
    注册流程图.png

    上述图片完成了如下流程:

1、安装了推送功能APP的设备,携带者设备号和APPid 连接APNs服务器。

2、连接成功后,APNs 通过打包和加密等处理生成devicetoken,返回给注册的设备。

3、APP拿到devicetoken后,将它发送给我们自己的服务器。

4、完成需要被推送的设备在苹果服务器和我们自己服务器之前的注册。

  • 推送过程
    推送过程.png

    上述图片完成如下流程:

1、首先,我们的设备安装了具有推送功能的APP后(APP在启动的时候会在lunch方法里注册推送),在有网络的情况下会连接到apns 推送服务器,在连接的过程中apns 会解密设备的devicetoken进行验证,验证成功后,建立tcp 连接。

2、Provider(我们自己的应用服务器)将 要被推送的消息结合接收消息的iOS设备的devicetoken发送给apns服务器

3、apns 收到Provider 发送过来的推送消息,解密devicetoken进行验证,验证成功后将消息发送给指定的设备

4、iOS设备收到苹果推送的消息后,通知我们的APP并显示和提示用户(alert,sound,badge)。
比较直观的流程图:


过程图.png

信息结构图:


图片来自网络.png

上图显示的这个消息体就是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.
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,846评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,146评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,789评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,656评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,467评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,870评论 3 389
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,500评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,790评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,828评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,628评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,449评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,881评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,077评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,376评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,879评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,089评论 2 338

推荐阅读更多精彩内容