一、设置 注册远程通知
[[UIApplication sharedApplication] registerForRemoteNotifications];
如果使用极光的话,[JPUSHService registerForRemoteNotificationConfig:entity delegate:self]
; 包装实现了上述api的功能
注意:
registerForRemoteNotifications
可以直接调用来注册远程推送,而不需要用户允许。也就是说只要调用该方法,就可以在AppDelegate的application:didRegisterForRemoteNotificationsWithDeviceToken:
中获取到设备的device token。- 那么通常的弹窗询问权限有什么用呢?其实只是请求用户允许在推送通知到来时能够有alert, badge和sound,而并不是在请求注册推送本身的权限。
- 用户不允许应用的推送,静默推送依然会送达用户设备,只是不会有alert, badge和sound。这也符合静默推送的正常使用场景。
二、处理 注册远程通知成功、失败回调
1 | /** |
有时候,会出现上面两个方法都不调用,导致推送接收不到。极光打印出Not get deviceToken yet.
可以从以下几个问题排查:
- 推送证书
- 是真机,且应用推送权限开启
- 确定调用了
registerForRemoteNotification
(不管是极光的还是原生的) - 网络问题
- 可以参考极光的文档:SDK FAQ
- 我的解决方案:飞行模式开启-关闭,然后就好使了(更新:飞行模式、关机重启已经解决不了这个网络问题了,只能设置-还原网络设置了,好使了…)
极光在能正确获取到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 | /** |
3.2 iOS 10及以后
1 | /** |
四、测试接收到APNS时,API的调用
1 | 实现方法1345 |
五、总结
- 方法1 2是在iOS 10之前,用来接收
remote notification
、local 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 10之前,默认App在前台运行时不会进行弹窗,使得需要在方法3中,判断是前台状态,然后发送
- 此外,一些版本的关于通知的特性:
- iOS 8
UIUserNotificationSettings
:修改了推送的注册接口,在原本的推送type的基础上,增加了一个categories
参数,这个参数的目的是用来注册一组和通知关联起来的button的事件。 - iOS 12,关于推送,又出了一些新特性:比如APP推送分组、消息左滑出现通知管理等
- 可以查看极光的汇总:https://docs.jiguang.cn/jpush/client/iOS/ios_new_fetures/
- iOS 8
六、静默推送
静默推送(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 | - (void)application:(UIApplication *)application |
退出APP超出两分钟,但是APP,没有被杀死、冻结的时候,状态栏上的是远程通知,走方法:
1 | - (void)application:(UIApplication *)application |
当APP被杀死,状态栏上的也算远程通知吧,走方法:
1 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions |
八、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运行
- 直接运行扩展的Target,会弹窗让你
- 方式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,系统将其初始化为单独的进程。
基于安全和性能的考虑,每一个扩展运行在一个单独的进程中,它拥有自己的bundle
, bundle
后缀名是.appex
。
iOS系统把扩展定义为 额外功能的触发入口点,它不是一个独立的App。因此,它必须依赖于宿主App,不能单独存在,也就没办法单独提审AppStore。
9.2 生命周期(Life Cycle)
- 用户触发“扩展”,如UI触发或者代码触发。
- iOS系统自动唤起“扩展”。
- 执行“扩展”代码。
- 执行完成后,系统杀死“扩展”,回收资源。
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、管道、XPC等,官方好像未明确说明。
- 疑似XPC。现象:扩展Target的源文件info.plist文件中,有个key CFBundlePackageType,有些扩展中值为
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
,有的直接显示为XPC!
(表示这个 bundle 是一个 XPC Service)
- 疑似XPC。现象:扩展Target的源文件info.plist文件中,有个key CFBundlePackageType,有些扩展中值为
一般,扩展与ContainingApp是无法直接通信的,例如扩展允许的时候,宿主App可能还未运行。受限的通信:
A Today widget
可以通过UrlSchemes的方式唤起ContainingApp。通过NSExtensionContext
的方法openURL:completionHandler:
。- 扩展和ContainingApp可以通过
shared container
实现间接、双向通信。- 虽然AppExtension的Bundle被导入如ContainingApp的包内,但是彼此的沙盒是没办法访问的。Apple发明了
share container
的中间层,来实现彼此的数据共享问题。也被称之为AppGroup
的概念。 - 这也映射软件工程的一句经典话语:多了个中间层,一切都显得那么美好。
- 基本原理如下:Apple允许 App进程 和 扩展进程 都可以对
SharedContainer
的共享数据区进行操作。
- 虽然AppExtension的Bundle被导入如ContainingApp的包内,但是彼此的沙盒是没办法访问的。Apple发明了