Tenloy's Blog

iOS APNS接收逻辑梳理

Word count: 4.8kReading time: 18 min
2020/07/01 Share

一、设置 注册远程通知

[[UIApplication sharedApplication] registerForRemoteNotifications];

如果使用极光的话,[JPUSHService registerForRemoteNotificationConfig:entity delegate:self]; 包装实现了上述api的功能

注意:

  • registerForRemoteNotifications可以直接调用来注册远程推送,而不需要用户允许。也就是说只要调用该方法,就可以在AppDelegate的application:didRegisterForRemoteNotificationsWithDeviceToken:中获取到设备的device token。
  • 那么通常的弹窗询问权限有什么用呢?其实只是请求用户允许在推送通知到来时能够有alert, badge和sound,而并不是在请求注册推送本身的权限。
  • 用户不允许应用的推送,静默推送依然会送达用户设备,只是不会有alert, badge和sound。这也符合静默推送的正常使用场景。

二、处理 注册远程通知成功、失败回调

1
2
3
4
5
6
7
/**
Typically, the system calls this method only after you call your WKExtension object’s registerForRemoteNotifications method, but WatchKit may call it under other rare circumstances. For example, WatchKit calls the method when the user launches an app after setting up the watch using a different device’s backup. In this case, the app doesn’t know the new device’s token until the user launches it.
如果这个方法不调用,那么APNS通知是收不到的,当然`应用内消息`可以照常使用
*/
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken API_AVAILABLE(ios(3.0));
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error API_AVAILABLE(ios(3.0));

有时候,会出现上面两个方法都不调用,导致推送接收不到。极光打印出Not get deviceToken yet. 可以从以下几个问题排查:

  • 推送证书
  • 是真机,且应用推送权限开启
  • 确定调用了registerForRemoteNotification(不管是极光的还是原生的)
  • 网络问题
    • 可以参考极光的文档:SDK FAQ
    • 我的解决方案:飞行模式开启-关闭,然后就好使了(更新:飞行模式、关机重启已经解决不了这个网络问题了,只能设置-还原网络设置了,好使了…)
极光:JPush iOS 调试思维导图

极光在能正确获取到deviceToken但收不到推送的情况:

我们是使用alias进行推送的,极光的逻辑是:

  • JPush SDK 注册完成之后,生成一个RegistrationID(与deviceToken关联,是会变化的,每次变化生成一个新的时,极光后台统计数据,会计入一个用户新增),然后设置别名的本质是将alias与RegistrationID关联起来。参考文档1参考文档2
  • 一个alias下可以绑定多个RegistrationID,如果一个别名被指定到了多个用户,当给指定这个别名发消息时,服务器端 API 会同时给这多个用户发送消息。
  • 如果我们为每台设备设置了唯一的一个别名(比如是idfa),但是用极光的API却查出这个别名下对应了多个设备(即RegistrationID),那么可能是在RegistrationID的有效期内没有调用deleteAlias:解除绑定(比如用户卸载,导致没机会解除)。极光提供了查询、删除接口来操作alias与RegistrationID之间的关系
  • 极光于 2020/03/10 对「别名设置」的上限进行限制,最多允许绑定 10 个设备,如果超出了,设置别名会失败
  • 如果RegistrationID变化之后,没有在该设备上再次调用setAlias:重新绑定alias与RegistrationID,就会导致通过alias找不到目前设备正确的RegistrationID,导致推送收不到
  • 综上所述,客户端与服务端之间使用RegistrationID标识,来推送,更为好用一点

三、处理 设备接收到通知的回调

3.1 iOS 10之前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 方法1: 兼容iOS 3 — iOS 10
* 过期后,分别由方法4 5来代替处理`用户可见远程通知`,由方法3来代替处理`静默远程通知silent remote notifications`
* If a remote notification arrives while your app is active, WatchKit calls this method to deliver the notification payload. 即只有当APP处于活跃状态时,收到远程通知,才会触发这个方法。
* 活跃分为:前台、后台活跃,当处于前台时不显示通知alert,直接触发这个方法(iOS 10之后如果想在前台显示alert,需要实现方法4,iOS10之前,则需要自己发local notification来显示alert)。当处于后台活跃状态时,会显示,点击通知才会触发这个方法
* 注意:如果实现了方法3,这个方法就不会被回调
*/
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo;

/**
* 方法2: 兼容iOS 4 — iOS 10
* 过期后, 分别由方法4 5来代替处理本地通知
* If a local notification arrives while your app is active, WatchKit calls this method to deliver the notification payload. 更详细的没测试
*/
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification;


/**
* 方法3: 兼容iOS 7及以后
* Tells the delegate that a background notification has arrived. 即APP处于挂起、kill状态都会触发这个方法。触发 与alert显示情况与方法1相同:处于前台时,不alert,直接调用;挂起或kill状态,点击通知会触发
* 实现此方法来处理传入的后台通知。当通知到达时,系统会启动您的应用程序或将其从挂起状态唤醒,您的应用程序会收到在后台运行的一小段时间。
* 可以使用后台执行时间处理通知并下载其相关内容。一旦完成对通知的处理,就必须调用fetchCompletionHandler完成处理程序。您的应用程序有30秒的挂钟时间来处理通知并调用处理程序,时间到了系统会终止您的应用程序。请尽可能快地调用处理程序,因为系统会跟踪应用程序后台通知的运行时间、功耗和数据成本。
* 后台通知是低优先级的,系统根据目标设备的功率考虑限制这些通知。APNs不保证设备会收到推送通知,而那些在处理后台通知时消耗大量能量或时间的应用程序可能不会收到后台时间来处理未来的通知。
* 应用程序因为远程通知而启动或恢复,也将调用此方法。注意,此方法与方法1冲突,如果实现了此方法,则不会调用方法1
*/
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler;

3.2 iOS 10及以后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 方法4: 兼容iOS 10及以后
* 只有当应用程序位于前台时,才会在收到通知时调用该方法。如果方法没有实现,或者没有及时调用处理程序,则前台不会显示通知。应用程序可以选择以声音、徽章、警报和/或在通知列表中显示通知。取决于通知中的信息是否对用户可见
* 实现了该方法后,方法1 3会收到影响,在前台时,不再是不显示+直接触发方法1 3,而是触发本方法,点击之后触发方法1 3
*/
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler;

/**
* 方法5: 兼容iOS 10及以后
* 当用户通过打开应用程序、取消通知或选择UNNotificationAction来响应通知时,会调用该方法。必须在应用程序从application:didFinishLaunchingWithOptions:返回之前设置delegate。
*/
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler;

/**
* 方法6: 兼容iOS 12及以后
* 极光的解释:当从应用外部通知界面或通知设置界面进入应用时,该方法将回调。
*/
- (void)userNotificationCenter:(UNUserNotificationCenter *)center openSettingsForNotification:(nullable UNNotification *)notification;

四、测试接收到APNS时,API的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
实现方法1345
前台
显示+调用willPresent 点击调用didReceiveNotificationResponse
后台/非active状态
显示+谁都不调 点击调用didReceiveNotificationResponse

实现方法135
前台
不显示+调用didReceiveRemoteNotification:fetchCompletionHandler
后台/非active状态
显示+谁都不调 点击调用didReceiveNotificationResponse

实现方法134
前台
显示+调用willPresent 点击调用didReceiveRemoteNotification:fetchCompletionHandler
后台/非active状态
显示+谁都不调 点击调用didReceiveRemoteNotification:fetchCompletionHandler

只实现方法1 3
前台
不显示+调用didReceiveRemoteNotification:fetchCompletionHandler
后台/非active状态
显示+谁都不调 点击调用didReceiveRemoteNotification:fetchCompletionHandler

五、总结

  • 方法1 2是在iOS 10之前,用来接收remote notificationlocal notification
  • 方法3,是为了实现Silent/Background Remote Notifications而出的API,但在功能的设计上,直接囊括了方法1的功能(当APNS消息中content-available = false时,功能与方法1相同),所以只要实现了方法3,方法1就不会再被调用。
    • 方法3比方法1功能更全面的一点是:方法1的方法是说明中,只有在active状态下,收到远程推送才调用。而方法3,在挂起、kill状态下,点击推送唤醒APP时,也会调用
  • 方法4 5是在iOS 10之后新出的框架<UserNotifications/UNUserNotificationCenter.h>用来取代之前的通知处理方式,并增加了很多新的特性,又将方法1的职责争取了过来,两者结合使用,使得方法123不再被调用(注意:在实现静默推送时,仍需用到方法3)
  • 当收到静默通知时,会自动触发方法3(无论是前台、后台、挂起状态)。
  • <UserNotifications.framework>库带来的特性:
    • iOS 10之前,默认App在前台运行时不会进行弹窗,使得需要在方法3中,判断是前台状态,然后发送local notification。iOS 10之后,只需要实现方法4,调用completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert);即可
    • UNNotificationServiceExtension,可以实现在通知展示之前拦截,自定义,实现通知上富文本、图片的展示。另外,附带着终于可以实现iOS的送达数据统计
    • getDeliveredNotificationsWithCompletionHandler 可以获取通知中心已展示的通知
    • 注意:在使用时,必须在didFinishLaunchingWithOptions返回之前设置代理:[UNUserNotificationCenter currentNotificationCenter].delegate = self;
  • 此外,一些版本的关于通知的特性:
    • iOS 8 UIUserNotificationSettings:修改了推送的注册接口,在原本的推送type的基础上,增加了一个categories参数,这个参数的目的是用来注册一组和通知关联起来的button的事件。
    • iOS 12,关于推送,又出了一些新特性:比如APP推送分组、消息左滑出现通知管理等
    • 可以查看极光的汇总:https://docs.jiguang.cn/jpush/client/iOS/ios_new_fetures/

六、静默推送

静默推送(silent/background remote notification)

iOS 7在推送方面最大的变化就是允许,应用收到通知后在后台(background)状态下运行一段代码,可用于从服务器获取内容更新。功能使用场景:(多媒体)聊天,Email更新,基于通知的订阅内容同步等功能,提升了终端用户的体验。用户不允许应用的推送,静默推送依然会送达用户设备。

如果只携带content-available: 1,不携带任何badge,sound 和消息内容等参数,alert字段可以有,但value必须为空,则可以不打扰用户的情况下进行内容更新等操作即为“Silent/Background Remote Notifications”,如果不携带此字段则是普通的Remote Notification。可以查看苹果文档Pushing Background Updates to Your App

客户端设置:需要在Xcode 中修改应用的Capabilities ,在Background Modes里面勾选Remote notifications(推送唤醒)

限制与注意:

  • “Silent Remote Notifications”是在 Apple 的限制下有一定的频率控制,但具体频率不详。所以并不是所有的 “Silent Remote Notifications” 都能按照预期到达客户端触发函数。
  • “Background”下提供给应用的运行时间窗是有限制的,如果需要下载较大的文件请参考 Apple 的 NSURLSession 的介绍。
  • 根据方法3的说明:系统会跟踪应用程序后台通知的运行时间、功耗和数据成本。background remote notification是低优先级的,系统根据目标设备的功率考虑限制这些通知。APNs不保证设备会收到推送通知,而那些在处理后台通知时消耗大量能量或时间的应用程序可能不会收到后台时间来处理未来的通知。
  • “Background Remote Notification” 的前提是要求客户端处于Background 或 Suspended 状态,如果用户通过 App Switcher 将应用从后台 Kill 掉应用将不会唤醒应用处理 background 代码。(这是极光文档中的说明,但是测试了一下,在前台收到也会触发方法3)
  • 静默推送:收到推送(没有文字没有声音),不用点开通知,不用打开APP,就能执行方法3,用户完全感觉不到
  • 注意:使用极光的Web页面进行推送测试,在iOS 13以下,无论是前台、后台状态都会触发方法3。但在iOS 13的系统中,只会在前台收到静默推送才会触发方法3(记录于2020-03-30)。查阅了一些资料,可能是apns-push-type: background 的问题(由于没有业务场景使用,并没有深究)。参考文档:iOS13 静默推送填坑Apple Developer Document—Pushing Background Updates to Your App

七、融云推送消息的发送机制

APP内运行的时候,不走通知机制

退出APP两分钟之内(没有杀死),状态栏上的是本地通知,走方法:

1
2
3
- (void)application:(UIApplication *)application
didReceiveLocalNotification:(UILocalNotification *)notification
}

退出APP超出两分钟,但是APP,没有被杀死、冻结的时候,状态栏上的是远程通知,走方法:

1
2
3
4
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo

}

当APP被杀死,状态栏上的也算远程通知吧,走方法:

1
2
3
4
5
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
// 远程推送的内容
NSDictionary *remoteNotificationUserInfo = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];

}

八、NotificationServiceExtension的集成与调试

NotificationServiceExtension属于AppExtension(扩展)的范畴,属于官方推出的扩展。同NotificationContentExtension一同丰富用户的推送体验,让开发者可以本地拦截和修改推送内容的机会,可以自定义自己的推送内容展示样式。大大提高了整体的用户体验。

  • 只允许修改推送内容,不能包含任何的UI显示。
  • 开发者无法自己初始化,在推送到达的时候iOS系统会自动创建UNNotificationServiceExtension对象。开发者只能通过扩展Target下的NotificationServiceExtension的子类来修改自己App推送的内容。
  • iOS收到App的推送的时候,系统自动初始化UNNotificationServiceExtension对象,然后回调给App的扩展子类的方法didReceiveNotificationRequest:withContentHandler:
  • 响应函数的操作时间只有30秒;如果未处理超时处理,系统会按照默认显示推送。
  • 服务端推送的payload格式要求:必须是alert类型。
    • aps字段必须包含有alert字段,alert里必须是title、subtitle、body任何一个。
    • aps字段必须包含有mutable-content字段,其值必须为1。

调试:(网上说的几种方式)

  • 方式1:(不生效)
    • 主工程编译,真机运行;
    • 扩展Target编译运行,选择主工程App,Attachment连接启动。
    • 模拟推送一波,UNNotificationServiceExtension的扩展之类,断点即可执行。
  • 方式2:(代码编译报错)
    • 直接运行扩展的Target,会弹窗让你Choose an app to run,选择主工程APP运行
  • 方式3:如何用 Xcode 来调试 App Extension?NICE!

不同的 App Extension 有不同的使用场景,这个启用了 App Extension 的场景(系统区域)叫做扩展点(extension point)。(App Extension 只会在其所属的扩展点被启动,所以如果要调试 App Extension,我们要先确定 App Extension 的扩展点。)

Notification Service Extension 的扩展点就是 在系统收到远程推送后至系统展示推送内容之前。我们需要确保该 Extension 的进程在这个场景被正常启动,随后我们才可以对其进行调试(在 Xcode 中为该 Extension 设置的断点才会被击中)。

  • 如果 App Extension 已经在运行,您可以通过 Xcode 的菜单 Debug - Attach to Process 浏览并查找该进程。
  • 不过,对于类似 Notification Service Extension 这种会在某个时期启动,然后自动停止的进程:
    • 一种方式:您可能需要在发送了远程推送后,尽快去 Attach to Process 列表寻找相关的进程。如果进程在被启动后很快又被终结了,导致您无法在列表中发现该进程,您也可以尝试让该进程睡眠一段时间,比如:sleep(10)
    • 另一种方式:如果您已经知道进程的 ID 或者名称,您可以直接进行查找: Debug - Attach to Process by PID or Name (对于推送扩展进程,这里是指 UNNotificationServiceExtension 的实现 Target 名称)。
      • 如果该进程未运行,调试器会一直等待。The debugger will wait for processes that aren’t running.
      • 如果报错,那么很可能是没有找对进程,建议先确认输入的名称或者 PID 是否正确。如果进程名存在重名情况,您就需要自行检查 Xcode 应该连接的是哪一个进程,通过PID来选定。

九、补充:AppExtension

9.1 概述

三个概念:

  • App Extension,Apple定义为”扩展“,也可以理解为”插件“。
  • Host App,能够调起extension的app被称为host app。例如,如果创建的是share extension,host app可能是Safari浏览器;如果创建的是widget extension,host app可能是系统的Today app。
  • Containing App,包含一个或者多个的Extension的App叫做ContainingApp,也叫做宿主App。

扩展不是独立App,系统将其初始化为单独的进程。

基于安全和性能的考虑,每一个扩展运行在一个单独的进程中,它拥有自己的bundlebundle后缀名是.appex

iOS系统把扩展定义为 额外功能的触发入口点,它不是一个独立的App。因此,它必须依赖于宿主App,不能单独存在,也就没办法单独提审AppStore。

9.2 生命周期(Life Cycle)

  1. 用户触发“扩展”,如UI触发或者代码触发。
  2. iOS系统自动唤起“扩展”。
  3. 执行“扩展”代码。
  4. 执行完成后,系统杀死“扩展”,回收资源。
app-extension

9.3 扩展与宿主APP的通信

  • Host App 与 ContainingApp 无法直接通信!

  • 扩展与Host App的通信是基于request/response模型。

    • request/response是通过 ”上下文“ 机制实现。HostApp为”扩展“提供运行的上下文(an extension context)。大致流程是,HostApp将 RequestData 通过上下文(an extension context) 输送给 扩展AppExtension,扩展进行处理(UI显示,用户交互,代码处理等),处理完成后将 ResponseData 返回给HostApp。
    • 由于”扩展“是一个独立的进程,一般一个App也是一个独立进程,因此它们间通信应该是基于进程间的通信方式。例如Socket、管道等,至于哪一种官方好像未说明(待研究)。app-extension2
  • 一般,扩展与ContainingApp是无法直接通信的,例如扩展允许的时候,宿主App可能还未运行。受限的通信:

    app-extension3
    • A Today widget 可以通过UrlSchemes的方式唤起ContainingApp。通过NSExtensionContext的方法openURL:completionHandler:
    • 扩展和ContainingApp可以通过shared container实现间接、双向通信。
      • 虽然AppExtension的Bundle被导入如ContainingApp的包内,但是彼此的沙盒是没办法访问的。Apple发明了share container的中间层,来实现彼此的数据共享问题。也被称之为AppGroup的概念。
      • 这也映射软件工程的一句经典话语:多了个中间层,一切都显得那么美好。
      • 基本原理如下:Apple允许 App进程扩展进程 都可以对SharedContainer的共享数据区进行操作。
app-extension3

Author:Tenloy

原文链接:https://tenloy.github.io/2020/07/01/ios-apns.html

发表日期:2020.07.01 , 11:30 AM

更新日期:2024.04.29 , 10:04 AM

版权声明:本文采用Crative Commons 4.0 许可协议进行许可

CATALOG
  1. 一、设置 注册远程通知
  2. 二、处理 注册远程通知成功、失败回调
  3. 三、处理 设备接收到通知的回调
    1. 3.1 iOS 10之前
    2. 3.2 iOS 10及以后
  4. 四、测试接收到APNS时,API的调用
  5. 五、总结
  6. 六、静默推送
  7. 七、融云推送消息的发送机制
  8. 八、NotificationServiceExtension的集成与调试
  9. 九、补充:AppExtension
    1. 9.1 概述
    2. 9.2 生命周期(Life Cycle)
    3. 9.3 扩展与宿主APP的通信