前言
我们今天主要通过文档参考+实验的方法来得出远程推送接收通知的那些方法调用的一些结论。文章较长,可以直接跳到最后看结论。
与远程推送的有关的几个方法
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions NS_AVAILABLE_IOS(3_0);
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo NS_DEPRECATED_IOS(3_0, 10_0, "Use UserNotifications Framework's -[UNUserNotificationCenterDelegate willPresentNotification:withCompletionHandler:] or -[UNUserNotificationCenterDelegate didReceiveNotificationResponse:withCompletionHandler:] for user visible notifications and -[UIApplicationDelegate application:didReceiveRemoteNotification:fetchCompletionHandler:] for silent remote notifications");
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler NS_AVAILABLE_IOS(7_0);
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler __IOS_AVAILABLE(10.0) __TVOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0) __OSX_AVAILABLE(10.14);
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0) __OSX_AVAILABLE(10.14) __TVOS_PROHIBITED;
方法1- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions NS_AVAILABLE_IOS(3_0);
这个做iOS的人都知道,是App启动的时候会调用的方法,值得一说的是,这个方法有个launchOptions 参数,可以用来标识启动的原因。点击了通知弹框打开App的时候,这个方法是会带有推送的参数的。
方法2:
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo NS_DEPRECATED_IOS(3_0, 10_0, "Use UserNotifications Framework's -[UNUserNotificationCenterDelegate willPresentNotification:withCompletionHandler:] or -[UNUserNotificationCenterDelegate didReceiveNotificationResponse:withCompletionHandler:] for user visible notifications and -[UIApplicationDelegate application:didReceiveRemoteNotification:fetchCompletionHandler:] for silent remote notifications");
一个非常古老的方法,在iOS7之前,推送主要是通过这个方法接收的。在iOS7之后就被下面的方法3替代了,而且在iOS10的时候这个方法已经被标为过时了,所以不建议使用。
方法3:
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler NS_AVAILABLE_IOS(7_0);
这个方法就比较重要了。iOS7的时候出来的方法,到iOS10之前都是称霸一方的大佬。当然由于静默推送的存在,直到现在仍然比较重要。除了能够完全替代方法2之外,还能处理推送唤醒的功能。关于推送唤醒(也叫做静默推送)这个功能比较复杂,本文不考虑这个功能。
方法4:
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler __IOS_AVAILABLE(10.0) __TVOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0) __OSX_AVAILABLE(10.14);
这个方法是iOS10推出的,属于UserNotifications
框架中UNUserNotificationCenterDelegate
代理协议的方法。主要是当App在前台的时候,收到推送通知之后调用。completionHandler回调可以控制是否显示alert、badge和sound。在iOS10之前,当App在前台的时候收到通知,是不会显示通知弹框的,但是在iOS10开始,实现了这个方法,我们可以选择是否显示通知弹框。比如在方法里面调用了completionHandler(UNNotificationPresentationOptionAlert);
,当App在前台收到通知也会显示通知弹框。可操作性更高了。
方法5:
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0) __OSX_AVAILABLE(10.14) __TVOS_PROHIBITED;
跟方法4配套的,同样是iOS10推出,属于UNUserNotificationCenterDelegate
的方法。主要是当App在后台以及关闭状态下收到通知之后通过点击通知打开App的时候调用。
测试工具
上面大概介绍了几个方法的基本情况。但是实际上这几个推送方法的关系实在是剪不断理还乱。所以为了搞清楚这些关系,我做了一大波的测试。
工欲善其事必先利其器,我们需要几下几个东西:
1.一个可以用来测试的demo,由于远程推送是需要推送证书的,所以需要一个开发者账号,BundleID需要配置了开启推送,并配置相应的推送证书。
2.一个发送推送通知的工具,我用的是github上star数很高的NWPusher里面的Mac App,可以使用这个App来发送通知,很好用。话说,这App的图标简直无力吐槽。。。
3.一个log工具,需要记录我们手动启动的时候打印的log(因为无法直接从Xcode上看),并且能够方便查看。我就直接用我之前做过的一个浮窗log工具了。
4.测试推送需要使用真机,目前手上容易拿到的是两个手机,系统分别为iOS9.3.2和iOS12.3.1。由于主要分界线是iOS10及iOS10之前,所以这两个够用了。
开始测试
首先我们默认demo已经调通了推送,能够拿到deviceToken了。
推送的payload为{"aps":{"alert":"Testing.. (0)","badge":1,"sound":"default"}}
(Puser这个App还很贴心地在每次推送完毕之后都会将Testing后面的数字加1,这样可以方便地辨认出那一次的通知)。静默推送的情况比较复杂,我们暂不测试。
上面的5个方法,我们只实现方法2(当然方法1一直都是要实现的,后面就不特别说明了)。
然后分别在App处于前台,App处于后台,App处于关闭状态的时候推送通知。以下为测试记录:
系统 | App处于前台 | App处于后台 | App处于关闭状态 |
---|---|---|---|
iOS9.3.2 | 不显示通知弹框,直接调用方法2 | 显示通知,通知到来时弹出推送弹框,点击弹框后打开App,调用方法2 | 显示通知,通知到来时弹出推送弹框,点击弹框后会调用方法1 |
iOS12.3.1 | 不显示通知弹框,直接调用方法2 | 显示通知,通知到来时弹出推送弹框,点击弹框后打开App,调用方法2 | 显示通知,通知到来时弹出推送弹框,点击弹框后会调用方法1 |
可以得出结论:
如果只实现方法2,不管是iOS10以下还是以上,推送通知到来时,
如果App处于前台,那么会直接调用方法2。
如果App处于后台,那么系统会显示通知提示框。如果我们不点击通知提示框,那什么都不会调用。如果点击了通知提示框,那么会打开App,并且会调用方法2。
当App在关闭状态,推送到来时,系统会弹出通知提示框。如果不点击通知提示框,那么什么都不会调用。如果点击了通知提示框,那么App会启动,将会调用方法1.方法1的launchOptions里面会有推送的参数。而方法2并不会调用。
接下测试的情况是只实现方法3:
系统 | App处于前台 | App处于后台 | App处于关闭状态 |
---|---|---|---|
iOS9.3.2 | 不显示通知弹框,直接调用方法3 | 显示通知,通知到来时弹出推送弹框,点击弹框后打开App,调用方法3 | 显示通知,通知到来时弹出推送弹框,点击弹框后会调用方法1,接着调用方法3 |
iOS12.3.1 | 不显示通知弹框,直接调用方法3 | 显示通知,通知到来时弹出推送弹框,点击弹框后打开App,调用方法3 | 显示通知,通知到来时弹出推送弹框,点击弹框后会调用方法1,接着调用方法3 |
结论:
如果只实现方法3,不管是iOS10以下还是以上,推送通知到来时,
当App处于前台,跟只实现方法2时相同
当App处于后台,跟只实现方法2时相同
当App在关闭状态,推送到来时,系统会弹出通知提示框。如果不点击通知提示框,那么什么都不会调用。如果点击了通知提示框,那么App会启动,将会调用方法1.方法1的launchOptions里面会有推送的参数。接着将会调用方法3,方法3的参数跟方法1的launchOptions参数是相同的(其实不是完全相同,launchOptions参数会用UIApplicationLaunchOptionsRemoteNotificationKey为key的字典将推送传过来的参数包裹住,也就是将方法3收到的参数包裹住)。
也就是说,只实现方法2和只显示方法3的不同点在于,当App处于关闭状态时,点击通知提示框之后,App在调用了方法1之后,会调用方法3,而方法2却不会被调用。
同时实现方法2和方法3:
系统 | App处于前台 | App处于后台 | App处于关闭状态 |
---|---|---|---|
iOS9.3.2 | 不显示通知弹框,直接调用方法3 | 显示通知,通知到来时弹出推送弹框,点击弹框后打开App,调用方法3 | 显示通知,通知到来时弹出推送弹框,点击弹框后会调用方法1,接着调用方法3 |
iOS12.3.1 | 不显示通知弹框,直接调用方法3 | 显示通知,通知到来时弹出推送弹框,点击弹框后打开App,调用方法3 | 显示通知,通知到来时弹出推送弹框,点击弹框后会调用方法1,接着调用方法3 |
结论:很明显,同时实现方法2和方法3,将会导致方法2被方法3覆盖,也就是说,方法2永远不会被调用。
只实现方法4和方法5
上文提过,方法4和方法5是配套使用的。所以现在要测试下只实现这两个方法的情况。先让方法4的参数设为0
系统 | App处于前台 | App处于后台 | App处于关闭状态 |
---|---|---|---|
iOS9.3.2 | 不会调用方法 | 显示通知,通知到来时弹出推送弹框,点击弹框打开App | 显示通知,通知到来内框会打开App,点击弹框后会调用方法1 |
iOS12.3.1 | 不显示通知弹框,直接调用方法4 | 显示通知,通知到来时弹出推送弹框,点击弹框后打开App,调用方法5 | 显示通知,通知到来时弹出推送弹框,点击弹框后会调用方法1,接着调用方法5 |
由于方法4和方法5是iOS10的时候才推出的,所以在iOS10以下的设备不生效。所以在iOS9.3.2设备上测试的时候这两个方法是无效的。在iOS10以上,这两个方法相当于将方法3拆分成了两个,在前台的时候调用方法4,在后台和关闭状态打开的时候调用方法5。
这里还需要测试一种情况,就是方法4收到推送之后,是可以弹出提示框的。我们把方法4的参数改为UNNotificationPresentationOptionAlert
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
{
NSLogD(@"willPresentNotification notification=%@",notification);
completionHandler(UNNotificationPresentationOptionAlert);
}
系统 | App处于前台 | App处于后台 | App处于关闭状态 |
---|---|---|---|
iOS12.3.1 | 调用方法4,显示通知,点击通知之后调用调用方法5 | 显示通知,通知到来时弹出推送弹框,点击弹框后打开App,调用方法5 | 显示通知,通知到来时弹出推送弹框,点击弹框后会调用方法1,接着调用方法5 |
也就是说,App在前台的时候,我们能够让通知显示提示框。点击提示框之后依旧会走方法5.
再来测一下一波,当我们将上面说的所有方法都实现了,然后测一波
系统 | App处于前台 | App处于后台 | App处于关闭状态 |
---|---|---|---|
iOS9.3.2 | 不显示通知,调用方法3 | 显示通知,通知到来时弹出推送弹框,点击弹框打开App,调用方法3 | 显示通知,通知到来内框会打开App,点击弹框后会调用方法1,然后调用方法3 |
iOS12.3.1 | 不显示通知弹框,直接调用方法4 | 显示通知,通知到来时弹出推送弹框,点击弹框后打开App,调用方法5 | 显示通知,通知到来时弹出推送弹框,点击弹框后会调用方法1,接着调用方法5 |
所以,跟推测中的结果一样,方法2不管怎样都不会调用,因为实现了方法3.在iOS10以下设备,方法3义不容辞地起作用了。在iOS10及以上设备,方法4和方法5都实现的情况下,优先调用方法4和方法5,而方法3将会被忽略掉。
到了这里,有没有想过,要是只实现方法5和方法3,或者只实现方法4和方法3,会怎么样呢?
我们只测试iOS10及以上设备,只实现方法5和方法3的情况
系统 | App处于前台 | App处于后台 | App处于关闭状态 |
---|---|---|---|
iOS12.3.1 | 不显示通知弹框,直接调用方法3 | 显示通知,通知到来时弹出推送弹框,点击弹框后打开App,调用方法5 | 显示通知,通知到来时弹出推送弹框,点击弹框后会调用方法1,接着调用方法5 |
只实现方法4和方法3的情况
系统 | App处于前台 | App处于后台 | App处于关闭状态 |
---|---|---|---|
iOS12.3.1 | 不显示通知弹框,直接调用方法4 | 显示通知,通知到来时弹出推送弹框,点击弹框后打开App,调用方法3 | 显示通知,通知到来时弹出推送弹框,点击弹框后会调用方法1,接着调用方法3 |
跟想象中一样,当方法4或方法5其中一个没实现的时候,备胎(方法3)就起作用了。
结论:
至此,我们就可以得出方法的调用顺序逻辑图了。
推送通知接收方法的调用跟系统版本(以iOS10作为分界线)、App状态(前台、后台、关闭状态)、方法是否实现(只实现了某些方法或者所有方法都实现)有关。
方法的优先级为
方法4和方法5 > 方法3 > 方法2
。系统检测到有优先级高的方法就调用,就不会管优先级较低的方法了。
所以在iOS10及以上系统,系统会先查找
[UNUserNotificationCenter currentNotificationCenter]
是否有delegate,delegate是否实现了方法4和方法5,如果有,就调用。如果没有,就查找方法3,有就调用。如果没有,再去查找方法2。
iOS10以下系统没有
UNUserNotification
框架,所以系统会直接找方法3,如果有,就调用。如果没有,就去查找方法2进行处理。
参考
总结 iOS 接收远程推送的响应方法
苹果推送功能官方文档
远程推送didReceiveRemoteNotification代理方法调用详细说明