iOS推送的那些事

关于推送

关于苹果的推送网上已经有非常多的资源讲解,我在这里就不再累赘。直接切入主题,讲讲如何模拟推送以及处理推送消息。在进入主题之前,我先说几个关键流程:

  1. 创建Push SSL Certification(推送证书)
  2. iOS客户端注册Push功能并获得DeviceToken
  3. 使用Provider向APNS发送Push消息
  4. iOS客户端接收处理由APNS发来的消息

推送流程图:

推送流程图
推送流程图

Provider:就是为指定iOS设备应用程序提供Push的服务器。如果iOS设备的应用程序是客户端的话,那么Provider可以理解为服务端(推送消息的发起者)
APNs:Apple Push Notification Service(苹果消息推送服务器)
Devices:iOS设备,用来接收APNs下发下来的消息
Client App:iOS设备上的应用程序,用来接收APNs下发的消息到指定的一个客户端app(消息的最终响应者)

获取Device token

App 必须要向 APNs 请求注册以实现推送功能,在请求成功后,APNs 会返回一个设备的标识符即 DeviceToken 给 App,服务器在推送通知的时候需要指定推送通知目的设备的 DeviceToken。在 iOS 8 以及之后,注册推送服务主要分为四个步骤:

  1. 使用 registerUserNotificationSettings:注册应用程序想要支持的推送类型
  2. 通过调用 registerForRemoteNotifications方法向 APNs 注册推送功能
  3. 请求成功时,系统会在应用程序委托方法中返回 DeviceToken,请求失败时,也会在对应的委托方法中给出请求失败的原因。
  4. 将 DeviceToken 上传到服务器,服务器在推送时使用。

上述第一个步骤注册的 API 是 iOS 8 新增的,因此在 iOS 7,前两个步骤需更改为 iOS 7 中的 API。

DeviceToken 有可能会更改,因此需要在程序每次启动时都去注册并且上传到你的服务器端。

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    if ([application respondsToSelector:@selector(registerUserNotificationSettings:)]) {
        NSLog(@"Requesting permission for push notifications..."); // iOS 8
        UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:
            UIUserNotificationTypeAlert | UIUserNotificationTypeBadge |
            UIUserNotificationTypeSound categories:nil];
        [UIApplication.sharedApplication registerUserNotificationSettings:settings];
    } else {
        NSLog(@"Registering device for push notifications..."); // iOS 7 and earlier
        [UIApplication.sharedApplication registerForRemoteNotificationTypes:
            UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge |
            UIRemoteNotificationTypeSound];
    }
    return YES;
}

- (void)application:(UIApplication *)application
    didRegisterUserNotificationSettings:(UIUserNotificationSettings *)settings
{
    NSLog(@"Registering device for push notifications..."); // iOS 8
    [application registerForRemoteNotifications];
}

- (void)application:(UIApplication *)application
    didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)token
{
    NSLog(@"Registration successful, bundle identifier: %@, mode: %@, device token: %@",
        [NSBundle.mainBundle bundleIdentifier], [self modeString], token);
}

- (void)application:(UIApplication *)application
    didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
    NSLog(@"Failed to register: %@", error);
}

- (void)application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier
    forRemoteNotification:(NSDictionary *)notification completionHandler:(void(^)())completionHandler
{
    NSLog(@"Received push notification: %@, identifier: %@", notification, identifier); // iOS 8
    completionHandler();
}

- (void)application:(UIApplication *)application
    didReceiveRemoteNotification:(NSDictionary *)notification
{
    NSLog(@"Received push notification: %@", notification); // iOS 7 and earlier
}

- (NSString *)modeString
{
#if DEBUG
    return @"Development (sandbox)";
#else
    return @"Production";
#endif
}

处理推送消息

  1. 当程序未启动,用户接收到消息。需要在AppDelegate中的didFinishLaunchingWithOptions得到消息内容

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        //...
        NSDictionary *payload = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
        if (payload) {
            //...
        }
       //...
    }
    
  2. 当程序在前台运行,接收到消息不会有消息提示(提示框或横幅)。当程序运行在后台,接收到消息会有消息提示,点击消息后进入程序,AppDelegate的didReceiveRemoteNotification函数会被调用,消息做为此函数的参数传入

    - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)payload {
        NSLog(@"remote notification: %@",[payload description]);
        NSString* alertStr = nil;        
        NSDictionary *apsInfo = [payload objectForKey:@"aps"];    
        NSObject *alert = [apsInfo objectForKey:@"alert"];    
        if ([alert isKindOfClass:[NSString class]])    
        {       
            alertStr = (NSString*)alert;    
        }    
        else if ([alert isKindOfClass:[NSDictionary class]])    
        {        
            NSDictionary* alertDict = (NSDictionary*)alert;        
            alertStr = [alertDict objectForKey:@"body"];    
        }        
        application.applicationIconBadgeNumber = [[apsInfo objectForKey:@"badge"] integerValue];        
        if ([application applicationState] == UIApplicationStateActive && alertStr != nil)    
        {
            UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:@"Pushed Message" message:alertStr delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
            [alertView show];    
        }
    }
    

自定义通知提示音

你可以在 App 的 Bundle 中加入一段自定义提示音文件。然后当通知到达时可以指定播放这个文件。必须为以下几种数据格式:

  • Linear PCM
  • MA4(IMA/ADPCM)
  • μLaw
  • aLaw

你可以将它们打包为aiff、wav或caf文件。自定义的声音文件时间必须小于 30秒,如果超过了这个时间,将被系统声音代替。

Payload

Payload 是通知的一部分,每一条推送通知都包含一个 Payload。它包含了系统提醒用户通知到达的方式,还可以添加自定义的数据。即通知主要传递的数据为 Payload。

Payload 本身为 JSON 格式的字符串,它内部必须要包含一个键为 aps 的字典。aps 中可以包含以下字段中的一个或多个:

alert:其内容可以为字符串或者字典,如果是字符串,那么将会在通知中显示这条内容
badge:其值为数字,表示当通知到达设备时,应用的角标变为多少。如果没有使用这个字段,那么应用的角标将不会改变。设置为 0 时,会清除应用的角标。
sound:指定通知展现时伴随的提醒音文件名。如果找不到指定的文件或者值为 default,那么默认的系统音将会被使用。如果为空,那么将没有声音。
content-available:此字段为 iOS 7 silent remote notification 使用。不使用此功能时无需包含此字段。

如果需要添加自定义的字段,就让服务器的小伙伴们跟aps同一层级添加一个数组(以Json为例):

{
  "aps" : {"alert" : "This is the alert text", "badge" : 1, "sound" :"default" },

  "server" : {"serverId" : 1, "name" : "Server name"}
}

这样收到的 Payload 里面会多一个 server 的字段。

模拟推送

现在常用的后台server中,一般将推送证书以及推送证书的私钥导出.p12交给后台人员即可。

生成后台需要的推送证书

准备:

  1. 苹果服务器证书端设置正确!打包证书、描述文件正确!!
  2. 下载推送证书(cer格式),导入keyChain,保证私钥存在,不存在去找创建这个证书的电脑要一份过来。
  3. 从钥匙串导出的根证书(推送证书)私钥(p12格式)

第三步根证书的私钥这里是一个坑!因为一个App的推送证书的创建可以和根证书创建的电脑不同,也就是keyChain产生的certSigningRequest不一样,所以私钥也是不一样的,在这里生成Pem时,注意要使用推送证书的私钥!

PHP需要的.Pem证书

PHP有点调皮,需要转换成pem

操作过程:

  1. 把推送证书(.cer)转换为.pem文件,执行命令:

    $ openssl x509 -in 推送证书.cer -inform der -out 推送证书.pem
    
  2. 把推送证书导出的私钥(.p12)文件转化为.pem文件:

    $ openssl pkcs12 -nocerts -out 推送证书私钥.pem -in 推送证书私钥.p12    
    
    $ Enter Import Password:  //先输入私钥的密码
    $ MAC verified OK
    $ Enter PEM pass phrase:  //设置两次相同的密码短语�
    $ Verifying – Enter PEM pass phrase:
    // 你首先需要为.p12文件输入pass phrase密码短语,这样OpenSSL可以读它。然后你需要键入一个新的密码短语来加密PEM文件。你需要选择一些更安全的密码短语。为了方便管理,例如我统一使用`pushchat`来作为PEM的密码短语。
    // 注意:如果你没有键入一个PEM passphrase,OpenSSL将不会返回一个错误信息,但是产生的.pem文件里面将不会含有私钥。
    
  3. 对生成的这两个pem文件再生成一个pem文件,来把证书和私钥整合到一个文件里:

    $ cat 推送证书.pem 推送证书私钥.pem >PHPPush.pem
    

然后把这个PHPPush.pem给后台基友们,就可以下班啦。

Java/.Net需要的.p12证书

若使用Java/.Net服务器需要将生成的私钥per文件和cer生成的per文件合并成一个.p12文件

前两个步骤同PHP需要的Pem证书步骤是一样的,只是第三个步骤有些许不同。

注意:第三个步骤这里还需要一个.certSigningRequest(csr)文件。

$ openssl pkcs12 -export -in  推送证书.pem -inkey 推送证书私钥.pem -certfile CertificateSigningRequest.certSigningRequest -name "项目名_Production(或项目名_Development)" -out 项目名_Production(或项目名_Development).p12

然后把这个项目名_Production(或项目名_Development).pem给后台基友们,就可以下班啦。

当然测试推送也比较麻烦,需要模拟真实的推送环境,一般需要后台提供帮助,但是遇到一些后台同事,他们有强烈地信仰着鄙视链的话,很鄙视iOS,心里早就称呼你“死前端”多年了,还那么多事……

所以关于调试推送,这里有两种方式实现自推!不麻烦别人。

模拟推送一:通过终端推送

<?php
// devicetoken
 $deviceToken = '你的deviceToken';
// 私钥密码,生成pem的时候输入的
$passphrase = '123456';
// 定制推送内容,有一点的格式要求,详情Apple文档
$message = array(
    'body'=>'你收到一个新订单'
);
$body['aps'] = array(
    'alert' => $message,
    'sound' => 'default',
    'badge' => 100,
    );
$body['type']=3;
$body['msg_type']=4;
$body['title']='新订单提醒';
$body['msg']='你收到一个新消息';

$ctx = stream_context_create();
stream_context_set_option($ctx, 'ssl', 'local_cert', 'push.pem');//记得把生成的push.pem放在和这个php文件同一个目录
stream_context_set_option($ctx, 'ssl', 'passphrase', $passphrase);
$fp = stream_socket_client(
    //这里需要特别注意,一个是开发推送的沙箱环境,一个是发布推送的正式环境,deviceToken是不通用的
    'ssl://gateway.sandbox.push.apple.com:2195', $err,
    //'ssl://gateway.push.apple.com:2195', $err,
    $errstr, 60, STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT, $ctx);
if (!$fp)
    exit("Failed to connect: $err $errstr" . PHP_EOL);
echo 'Connected to APNS' . PHP_EOL;
$payload = json_encode($body);
$msg = chr(0) . pack('n', 32) . pack('H*', $deviceToken) . pack('n', strlen($payload)) . $payload;
$result = fwrite($fp, $msg, strlen($msg));
if (!$result)
    echo 'Message not delivered' . PHP_EOL;
else
    echo 'Message successfully delivered' . PHP_EOL;
fclose($fp);
?>

将上面的代码复制,保存成push.php

然后根据上面PHP需要的Pem证书的步骤生成push.pem

两个文件放在同一目录

执行下面的命令

superdanny@SuperDannydeMacBook-Pro$ php push.php 

结果为

Connected to APNS
Message successfully delivered

模拟推送二:通过工具推送

Github 上面有位大神分享了他的推送工具NWPusher,大大减少了开发人员的工作量。具体用法说明上面写的很清楚,这里就不再重复。

补充

2016年02月18日

  1. 昨天由于有个应用的推送证书过期了,所以需要重新生成新的推送证书。偶然之间发现苹果的推送证书更新了,一开始还觉得有些跟以前不同,在这里记录一下,方面后人不入坑。

    Apple Push Notification Service Update
    December 17, 2015
    At WWDC 2015 we announced a new HTTP/2 based provider API and simplified certificate management process for developers using the Apple Push Notification service. You can now take advantage of this new provider API and create a single SSL certificate for each of your iOS apps, allowing you to connect to both APNs environments. To learn more, read the Local and Remote Notification Programming Guide and watch What’s New in Notifications.

    新旧推送证书不同

2016年02月20日

当上线产品推送证书过期时,不要惊慌,我们不需要更新推送证书之后重新提交 AppStore 。只需要替换服务器端的推送证书即可(记得重启服务器,不然推送在测试环境下有效,但是在真实环境下是无效的)。

2016年12月27日

iOS 10 已经出了好长一段时间,一直想补充这一块推送的差异,今天趁这个空闲,补充一下。
从 iOS 10 新增的 UserNotifications Framework 可以发现,Apple 整合了原有散乱的 API,并且增加了许多强大的功能。以 Apple 官方的角度来看,也必然是相当重视推送服务对 App 的影响、以及对 Apple iOS 生态圈长远发展的影响。

这里直接引用别人写的博文,个人认为写的很全面,这边就不做展开。

玩转 iOS 10 推送 —— UserNotifications Framework(上)
玩转 iOS 10 推送 —— UserNotifications Framework(中)
玩转 iOS 10 推送 —— UserNotifications Framework(下)


再一次感谢您花费时间阅读这篇文章!

微博: @Danny_吕昌辉
博客: SuperDanny

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

推荐阅读更多精彩内容