iOS推送之远程推送(iOS Notification Of Remote Notification)

注:此文只现在已经不能适配iOS10了,iOS10推送采用了新的方法,做iOS9及以下的系统可读此篇文章。

最近公司项目升级重构(重写),除了本来我所负责的模块,最后临危受命接了推送(远程和本地)相关的模块,顺便把推送的相关知识复习了一遍。后期连续工作十几天加上最后一天的通(瞎)宵(熬)达(一)旦(夜),也算是不辱使命。此文除了讲解远程推送相关的基本知识外,也会涉及一些推送相关的奇淫技巧。另外本文主要讲解远程推送,后续会出一篇iOS推送之本地推送(iOS Notification Of Local Notification)的姊妹篇。

此篇文章的逻辑如下图所示:

图0-0 此篇文章的逻辑图

远程推送原理

学习一些东西前我认为最好能了解它的原理,这样以后我们遇到问题的时候,就可以很快速的找到错误之所在,如果对原理不感兴趣的同学可直接下翻到应用部分【远程推送应用】。

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

我们的设备联网时(无论是蜂窝联网还是Wi-Fi联网)都会与苹果的APNs服务器建立一个长连接(persistent IP connection),当Provider推送一条通知的时候,这条通知并不是直接推送给了我们的设备,而是先推送到苹果的APNs服务器上面,而苹果的APNs服务器再通过与设备建立的长连接进而把通知推送到我们的设备上(参考图1-1,图1-2)。而当设备处于非联网状态的时候,APNs服务器会保留Provider所推送的最后一条通知,当设备转换为连网状态时,APNs则把其保留的最后一条通知推送给我们的设备;如果设备长时间处于非联网状态下,那么APNs服务器为其保存的最后一条通知也会丢失。Remote Notification必须要求设备连网状态下才能收到,并且太频繁的接收远程推送通知对设备的电池寿命是有一定的影响的。

图1-1 Pushing a remote notification from a provider to a client app
图1-2 Pushing remote notifications from multiple providers to multiple devices

deviceToken的生成

当一个App注册接收远程通知时,系统会发送请求到APNs服务器,APNs服务器收到此请求会根据请求所带的key值生成一个独一无二的value值也就是所谓的deviceToken,而后APNs服务器会把此deviceToken包装成一个NSData对象发送到对应请求的App上。然后App把此deviceToken发送给我们自己的服务器,就是所谓的Provider。Provider收到deviceToken以后进行储存等相关处理,以后Provider给我们的设备推送通知的时候,必须包含此deviceToken。(参考图1-3,图1-4)

图1-3 Managing the device token
图1-4 Sharing the device token

这个时候你可能会问deviceToken到底是什么?有什么用?为什么是独一无二的?

  • 是什么:deviceToken其实就是根据注册远程通知的时候向APNs服务器发送的Token key,Token key中包含了设备的UDID和App的Bundle Identifier,然后苹果APNs服务器根据此Token key编码生成一个deviceToken。deviceToken可以简单理解为就是包含了设备信息和应用信息的一串编码。
  • 有什么用:上面提到Provider推送消息的时候必须带有此deviceToken,然后此消息就根据deviceToken(UDID + App's Bundle Identifier)找到对应的设备以及该设备上对应的应用,从而把此推送消息推送给此应用。
  • 唯一性:苹果APNs的编码技术和deviceToken的独特作用保证了他的唯一性。唯一性并不是说一台设备上的一个应用程序永远只有一个deviceToken,当用户升级系统的时候deviceToken是会变化的。

<a id="远程推送应用"></a>远程推送应用

注册远程通知(获取deviceToken)

注册远程通知的方法

一般都是在App启动完成的时候去注册远程通知注册方法调用一般都在didFinishLaunchingWithOptions:方法中

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 在iOS8之前注册远程通知的方法,如果项目要支持iOS8以前的版本,必须要写此方法
    UIRemoteNotificationType types = UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert;

    [[UIApplication sharedApplication] registerForRemoteNotificationTypes:types];

    // iOS8之后注册远程通知的方法
    UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert;
    
    UIUserNotificationSettings *mySettings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
    
    [[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];
}

处理注册远程通知的回调方法

// 注册成功回调方法,其中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这几个类。

处理接收到远程通知消息(会回调以下方法中的某一个)

application: didFinishLaunchingWithOptions:

此方法在程序第一次启动是调用,也就是说App从Terminate状态进入Foreground状态的时候,根据方法内代码判断是否有推送消息。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    //  userInfo为收到远程通知的内容
    NSDictionary *userInfo = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
    if (userInfo) {
         // 有推送的消息,处理推送的消息
    }
    return YES;
}
application: didReceiveRemoteNotification:

如果App处于Background状态时,只用用户点击了通知消息时才会调用该方法;如果App处于Foreground状态,会直接调用该方法。

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
  
}
application: didReceiveRemoteNotification: fetchCompletionHandler:

iOS7之前苹果是不支持多任务的,这也是iOS系统对硬件要求低,流畅性好的原因之一。iOS7之后,苹果开始支持多任务,即App可在后台做一些更新UI、下载数据的操作等。若要接收到远程推送的时候要在后台做一些事情则需要把后台远程推送模式打开。不适配iOS7之前系统的项目建议使用此后台模式,充分利用苹果推出的多任务模式,不枉费苹果的一片苦心啊!设置后台模式方法项目对应TARGETS-Capabilities-Background Modes-Remote Notifications具体设置方法如下图(图2-1)。

图2-1 Setting App Background Modes

此方法不论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 {

}

客户端和服务端的交互

说到这里我就随意吐槽一下推送,做推送个人感觉还是比较费劲的。而第一次启动App时询问用户是否接受推送消息的时候,大部分用户都会点击拒绝推送的吧,反正我是这样的。你辛辛苦苦做好了,想办法保证其推送准时性,想办法保证其推送到达率,结果用户一个拒绝,你所以的努力全都白费了啊,哈哈哈。

我这里主要想说的就是:我们要把对应的.p12(个人信息交换证书)证书给服务端的开发人员就好了。具体可参看我另一篇文章不让苹果开发者账号折磨我中的团队开发证书的管理中的导出.p12章节。

远程推送负载

远程推送负载大小

远程通知负载的大小根据Provider使用的API不同而不同。当使用HTTP/2 provider API时,负载最大为4096bytes,即4kB;当使用legacy binary interface时,负载最大为2048bytes,即2kB。当负载大小超过规定的负载大小时,APNs会拒绝发送此消息。

<a id="远程推送负载内容"></a>远程推送负载内容

内容格式必要要知道的啊,服务端一般会要我们客户端定义好格式给他们的。

每一条通知的消息都会组成一个JSON字典对象,其格式如下所示,示例中的key值为苹果官方所用key。自定义字段的时候要避开这些key值。

{
     "aps" : {  
        "alert"              :              {   // string or dictionary
          "title"          :   "string"
            "body"           :   "string",
            "title-loc-key"  :   "string or null"
            "title-loc-args" :   "array of strings or null"
            "action-loc-key" :   "string or null"
            "loc-key"        :   "string"
            "loc-args"       :   "array of strings"
            "launch-image"   :   "string"
        },
        "badge"             :    number,
        "sound"             :    "string"
        "content-available" :    number;
        "category"          :    "string"
     },
}

aps:推送消息必须有的key

alert:推送消息包含此key值,系统就会根据用户的设置展示标准的推送信息
badge:在app图标上显示消息数量,缺少此key值,消息数量就不会改变,消除标记时把此key对应的value设置为0
sound:设置推送声音的key值,系统默认提示声音对应的value值为default
content-available:此key值设置为1,系统接收到推送消息时就会调用不同的回调方法,iOS7之后配置后台模式
category:UIMutableUserNotificationCategory's identifier 可操作通知类型的key值

title:简短描述此调推送消息的目的,适用系统iOS8.2之后版本
body:推送的内容
title-loc-key:功能类似title,附加功能是国际化,适用系统iOS8.2之后版本
title-loc-args:配合title-loc-key字段使用,适用系统iOS8.2之后版本
action-loc-key:可操作通知类型key值,不详细叙述
loc-key:参考title-loc-key
loc-args:参考title-loc-args
launch-image:点击推送消息或者移动事件滑块时,显示的图片。如果缺少此key值,会加载app默认的启动图片。

当然以上key值并不是每条推送消息都必带的key值,应当根据需求来选择所需要的key值,除了以上系统所提供的key值外,你还可以自定义自己的key值,来作为消息推送的负载,自定义key值与aps此key值并列。如下格式:


{
    "aps" : {
        "alert" : "Provider push messag.",
        "badge" : 9,
        "sound" : "toAlice.aiff"
    },
    "Id"   : 1314,               //  自定义key值
    "type" : "customType"        //  自定义key值
}

指定用户的推送

对于要求用户登录的App,推送是可以指定用户的,同一条推送有些用户可以收到,但是有些用户又不能收到。说起来这个就要提到另外的一个token了,一般称之为userToken,userToken一般都是根据自己公司自定义的规则去生成的。userToken是以用户的账号加对应的密码生成的。这样结合上面提到的deviceToken,就可以做到根据不同的用户推送不同的消息。deviceToken找到对应某台设备和该设备上的应用,而userToken对应找到该用户。客户端在上报deviceToken的时候,要把userToken对应一起上报给服务端也就是Provider。

浅谈推送第三方SDK

关于第三方推送的SDK有很多,常见的有极光推送 百度推送 个推 友盟推送等等。其实推送的原理都是大同小异的,理解了苹果推送的原理,这些第三方SDK还在是基本原理上面进行了扩展。对于用不用第三方SDK其实对我们客户端影响不大,推送第三方SDK主要是方便了服务端开发者。主要表现为服务端开发者不需要去开发维护自己的推送服务器与 APNs 对接,不必自己维护更新 deviceToken。当然了,第三方SDK也会提供一些额外的附属功能例如JPush提供了应用内消息推送,这在类似于聊天的场景里很方便的。看完这段是不是发现集成推送的第三方SDK和客户端没什么关系,我们工作量不仅没有减少,反而增加了一点点啊。至于第三方SDK的其他功能,大家可自行去对应官网学习,这里不再过多描述。

利用runtime实现推送消息万能跳转

此段参考了@汉斯哈哈哈的一篇iOS 万能跳转界面方法万能跳转就是可以跳转到指定的任意一个界面,但是这个和服务端耦合性太强,使用的时候要慎重考虑,而且公司一般都是iOS,Android共用同一套推送规则很难让服务端在给你开一条新的推送规则,不便于维护,而且成本也是需要考虑的。写此段的目的就是当产品有这样的需求的时候还是可以参考一下的。

定义推送规则

// 客户端控制器的属性
@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"
    }
}

跳转逻辑

// 接收到推送后跳转
- (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;
}

总结

好好理解远程推送的原理就会发现,其实远程推送并没有那么难做啊。上面的一些图片有些来源于苹果官方文档,有些是自己所截图。一些知识也是参考了苹果的官网文档。其中一些深入的推送相关知识普遍性不是太高,所以也没有提到,例如:可操作通知类型,通知显示国际化,自定义通知声音,Provider-APNs-Device详细连接情况及推送负载的底层数据格式等。如果你对这些知识很感兴趣也很欢迎私密我私下交流,共同进步。敬请期待本篇的姊妹篇iOS推送之本地推送(iOS Notification Of Local Notification)

参考文献

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

推荐阅读更多精彩内容