细说 iOS 消息推送

经常有同学问我们,iOS 上推送究竟怎么做啊,为什么我的设备总收不到推送呢,这里跟大家集中讨论一下 iOS 上推送的实现细节。

APNs 的推送机制

与 Android 上我们自己实现的推送服务不一样,Apple 对设备的控制非常严格,消息推送的流程必须要经过 APNs:
remote_notif_simple_2x

这里 Provider 是指某个应用的 Developer,当然如果开发者使用 LeanCloud 的服务,把发送消息的请求委托给我们,那么这里的 Provider 就是 LeanCloud 的推送服务程序了。上图可以分为三步:

  1. LeanCloud 推送服务程序把要发送的消息、目的设备的唯一标识打包,发给 APNs。
  2. APNs 在自身的已注册 Push 服务的应用列表中,查找有相应标识的设备,并把消息发送到设备。
  3. iOS 系统把发来的消息传递给相应的应用程序,并且按照设定弹出 Push 通知。

为了实现消息推送,有两点非常重要:

  1. App 的推送证书
    要能够完整实现一条消息推送,需要我们在 App ID 中打开 Push Notifications,需要我们准备好 Provisioning Profile 和 SSL 证书,并且一定要注意 Development 和 Distribution 环境是需要分开的。最后,把 SSL 证书导入到 LeanCloud 平台,就可以尝试远程消息推送了。具体的操作流程可以参考我们的使用指南:iOS 推送证书设置指南
  2. 设备标识 DeviceToken
    知道了谁要推送,或者说要推送给哪个应用之后,APNs 还需要知道推到哪台设备上,这就是设备标识的作用。获取设备标识的流程如下:

    1. 应用打开推送开关,用户要确认 TA 希望获得该应用的推送消息;
    2. 应用获得一个 DeviceToken;
    3. 应用将 DeviceToken 保存起来,这里就是通过 [AVInstallation saveInBackground] 将 DeviceToken 保存到 LeanCloud;
    4. 当某些特定事件发生,开发者委托 LeanCloud 来发送推送消息,这时候 LeanCloud 的推送服务器就会给 APNs 发送一则推送消息,APNs 最后消息送到用户设备。

推送相关的几个概念

消息类型

一条消息推送过来,可以有如下几种表现形式:

  • 显示一个 alert 或者 banner,展现具体内容。
  • 在应用 icon 上提示一个新到消息数。
  • 播放一段声音。

开发者可以在每次推送的时候设置,在推送达到用户设备时开发者也可以选择不同的提示方式。

本地消息通知

iOS 上有两种消息通知,一种是本地消息(Local Notification),一种是远程消息 (Push Notification,也叫 Remote Notification),设计这两种通知的目的都是为了提醒用户,现在有些什么新鲜的事情发生了,吸引用户重新打开应用。

本地消息什么时候有用呢?譬如你正在做一个 To-do 的工具类应用,对于用户加入的每一个事项,都会有一个完成的时间点,用户可以要求这个 To-do 应用在事项过期之前的某一个时间点提醒一下 TA。为了达到这一目的,应用就可以调度一个本地通知,在时间点到了之后发出一个 Alert 消息或者其他提示。

我们在处理推送消息的时候,也可以综合运用这两种方式。

代码里面如何实现推送

首先,我们要获取 DeviceToken。

应用需要每次启动的时候都去注册远程通知——通过调用 UIApplication 的 registerForRemoteNotificationTypes: 方法,传递给它你希望支持的消息类型参数即可,例如:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // do some initiale working
    ...

    [application registerForRemoteNotificationTypes:UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeSound];
    return YES;
}

如果注册成功,APNs 会返回给你设备的 token,iOS 系统会把它传递给 app delegate 代理 application:didRegisterForRemoteNotificationsWithDeviceToken: 方法,你应该在这个方法里面把 token 保存到 LeanCloud 后台,例如:

- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    NSLog(@"Receive DeviceToken: %@", deviceToken);
    AVInstallation *currentInstallation = [AVInstallation currentInstallation];
    [currentInstallation setDeviceTokenFromData:deviceToken];
    [currentInstallation saveInBackground];
}

如果注册失败,application:didFailToRegisterForRemoteNotificationsWithError: 方法会被调用,通过 NSError 参数你可以看到具体的出错信息,例如:

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
    NSLog(@"注册失败,无法获取设备 ID, 具体错误: %@", error);
}

请注意,注册流程需要在应用每次启动时调用,这并不不会带来额外的负担,因为 iOS 操作系统在第一次获得了有效的 device token 之后,会本地缓存起来,以后应用再调用 registerForRemoteNotificationTypes: 的时候会立刻返回,并不会再进行网络请求。另外,应用层面不应该对 device token 进行缓存,因为 device token 也有可能变化——如果用户重装了操作系统,那么 APNs 再次给出的 device token 就会和之前的不一样,又或者是,用户 恢复了原来的备份到新的设备上,那么原来的 device token 也会失效。

其次,我们要处理收到消息之后的回调。

我们可以设想一下消息通知的几种使用场景:

  1. 在应用没有被启动的时候,接收到了消息通知。这时候操作系统会按照默认的方式来展现一个 alert 消息,在应用的 icon 上标记一个数字,甚至播放一段声音。
  2. 用户看到消息之后,点击了一下 action 按钮或者点击了应用图标。如果 action 按钮被点击了,系统会通过调用 application:didFinishLaunchingWithOptions: 这个代理方法来启动应用,并且会把 notification 的 payload 数据传递进去。如果应用图标被点击了,系统也一样会调用 application:didFinishLaunchingWithOptions: 这个代理方法来启动应用,唯一不同的是这时候启动参数里面不会有任何 notification 的信息。示例代码如下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // do initializing works
    ...

    if (launchOptions) {
        // do something else
        ...

        [AVAnalytics trackAppOpenedWithLaunchOptions:launchOptions];
    }

    [application registerForRemoteNotificationTypes:UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeSound];

    return YES;
}

3、如果远程消息发送过来的时候,应用正在运行,这时候会发生什么呢?应用代理的 application:didReceiveRemoteNotification: 方法会被调用,同时远程消息中的 payload 数据会作为参数传递进去。示例代码如下:

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    if (application.applicationState == UIApplicationStateActive) {
        // 转换成一个本地通知,显示到通知栏,你也可以直接显示出一个 alertView,只是那样稍显 aggressive:)
        UILocalNotification *localNotification = [[UILocalNotification alloc] init];
        localNotification.userInfo = userInfo;
        localNotification.soundName = UILocalNotificationDefaultSoundName;
        localNotification.alertBody = [[userInfo objectForKey:@"aps"] objectForKey:@"alert"];
        localNotification.fireDate = [NSDate date];
        [[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
    } else {
        [AVAnalytics trackAppOpenedWithRemoteNotificationPayload:userInfo];
    }
}

常见问题 FAQ

我能推送长消息吗?

不能,APNs 限制了每个 notification 的 payload 最大长度是 256 字节,超长的消息是不能发送的。

推送怎么加声音提醒?

消息推送是可以指定声音的。譬如你可以对正面的反馈使用欢快的声音,对负面的反馈使用低沉一点的声音,都可以达到别出心裁让人眼前一亮的目的。你需要先放一些 aiff、wav 或者 caf 音频文件到应用的资源文件中,然后在推送的时候指定不同的音频文件名就可以了。

sendWithSound_2x

推送的 Badge 是怎么回事?

推送并不一定会导致应用图标上红色数字增加,是否显示这一数字,显示成多少,都取决于开发者自己。在发送推送消息的时候,我们可以选择是否递增这一数字;如果不选择这一项,那么消息推送并不会导致应用图标上红色数字的出现。

sendWithBadge_2x

好,现在问题来了,这个数字如果搞出来了,怎么让它消失掉呢?其实我们只需要在任何时候设置 UIApplication.applicationIconBadgeNumber 属性为 0,就可以让这个数字消失掉。

一般我们会选择在应用启动的时候(application:didFinishLaunchingWithOptions: 方法中),或者干脆一点,在应用每次被切换到前台的时候(applicationWillEnterForeground: 方法中),调用这一行代码,即可立刻清除掉 Badge 数字了。

LeanCloud 平台发出去的通知格式究竟是什么样子?

对于每一条推送消息,都包含一个 payload,通常是组成了一个 JSON 的 Dictionary,这其中必不可少的是 aps 属性,它对应的 value 也是一个 Dictionary,包含下面一些内容:

  • alert 消息(文本或 Dictionary)
  • 应用图标上的红色数字
  • 播放的声音文件名

在由推送激活的应用打开事件中,application:didFinishLaunchingWithOptions: 的 options 参数就是这个大的 Dictionary 对象。

{
    aps =     {
        alert = "hello, everyone";
        badge = 2;
        sound = default;
    };
}

这里要注意的时 alert 部分,它的值可以是一个 String(文本消息),也可以是一个 JSON 的 Dictionary。当它是文本消息的时候,系统就会把这些文字显示到一个 AlertView 中;如果它也是由一个 JSON Dictionary 组成的话,其格式如下:

  • body
  • action-loc-key
  • loc-key
  • loc-args
  • launch-image

body 部分就是 AlertView 中将要展现出来的文本消息,loc- 属性主要是用来实现本地化消息,launch-image 只是应用主 bundle 里的一个图片文件的名称,一般来说我们不指定这一属性。

如何显示本地化的消息?

有两种办法可以实现推送消息的本地化:

  1. 在推送的 payload 中使用 loc-keyloc-args 来指定进行本地化,这样 Provider 方只需要按照统一的格式来发送即可,消息的解析和组装则由客户端来完成。
  2. 如果推送的 payload 里面不包含 loc-key/loc-args 信息,那么 Provider 方就需要自己做本地化处理,然后给不同的 device 发送不同的消息,为了做到这一点,还需要应用在上传 device token 的时候也把用户的语言设置信息传回来。

目前,因为 LeanCloud 主要就是瞄准中国大陆市场和海外中文用户,所以我们在推送上还不提供多语言支持。

应用该怎么响应推送消息?

上面说的处理流程,只能简单展示一下远程消息,激活用户让他们重新回到应用中来。但是有时候,我们希望带给用户更好的使用体验,譬如如果我们告诉用户:「张三刚刚评论了你的照片」。这时候用户如果点击 action 按钮进入应用,我们是展示具体的评论页面为好,还是展示通常的启动页面然后让用户自己去找张三的评论好?我想负责任的开发者都会选择前者。:)

要做到灵活响应不同类型的通知消息,我们需要在通知的 payload 中增加更多信息,而不能仅仅只有 alert 出来的文字信息。对于 LeanCloud 消息推送平台来讲,就需要开发者使用更高级功能的 JSON 格式。譬如我们发送这样的 JSON 字符串

{"action":{"type":4},"alert":"hello, everyone"} 最终在应用内会收到这样的 UserInfo Dictionary:

{
    action =     {
        type = 4;
    };
    aps =     {
        alert = "hello, everyone";
        badge = 4;
    };
}

「hello, everyone」会显示到 AlertView 中,但是整个 Dictionary 会通过 launchOptions 传递给 application:didFinishLaunchingWithOptions: 方法,这样我们在程序里面就可以对不同的消息实现不同的跳转了。

细说 iOS 消息推送》上有18条评论

    1. nambypamby

      你可以看下这个 Link, 里面有详细的证书生成安装 http://www.cnblogs.com/qq78292959/archive/2012/07/16/2593651.html

      回复
  1. weak

    我遇到一个问题, 希望帮我解决一下 , 我创建了一个应用, 需要配置的都配置了, 运行程序的时候,devicetoken 也是正常的保存在了服务器里面, 但是当推送消息的时候, 显示的是推送成功了, 但是手机上没有任何的反应, 不知道这是什么原因

    回复
    1. expll

      是啊, 消息显示是推送成功了, 但是为啥手机没有收到呢!
      ID 内容 状态 invalidTokens (iOS) 设备数 创建时间
      09fUUM9Uhob2FAWF The Mets scored! The game is now tied 1-1! 完成推送 2

      回复
  2. 小蜗牛

    你好 请问如何关闭 LeanCloud 的推送呢?
    我们 app 当中有两种推送 一种是 Leancloud 的 IM 消息 离线推送 另一种是自己服务器实现的推送
    现在想做一个按钮 关闭 LeanCloud 的 IM 消息推送 我应该调哪一个接口呢?是 AVPush 当中的吗?

    回复
    1. Junwen FengJunwen Feng
      管理员
      文章作者

      对于聊天消息的离线推送,我们有两个解决方案:第一个,交给 LeanCloud 来自动帮你做,你只需要在控制台的「消息」「实时消息」「设置」里面的「iOS 用户离线推送设置」中填上内容即可;第二种,就是完全由你自己来控制,可以响应我们的 webHook(参见这里:https://leancloud.cn/docs/realtime_v2.html#云引擎_Hook),重点看 _receiversOffline(https://leancloud.cn/docs/realtime_v2.html#_receiversOffline)这个方法,通过我们的 JS API 来完全控制如何进行推送。

      回复
    1. jfeng@avoscloud.comjfeng@avoscloud.com

      在 Installation 表里面有一个 timezone 的列,你可以把它加到推送设备的选择条件里面,这样对不同时区的用户进行不同的推送。

      回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注