对用户而言,电子邮件客户端是一个简单的应用程序。 Yandex.Mail开发人员甚至开玩笑说,应用程序只有三个屏幕:字母列表; 寄信 关于屏幕。
但是很多有趣的事情正在幕后发生。 与许多移动应用程序一样,Mail使用推式通知与用户进行交互。 与许多iOS应用程序一样,由于Apple Push Notification Service的性质,Mail丢失了一些通知。
Yandex.Mail iOS小组负责人
Asya Sviridenko将证明,即使系统受到限制,如果推送通知对于您的应用程序至关重要,则仍然有可能和必要处理推送通知的丢失。 对于Mail来说确实如此,因为新字母的推送通知是用户为其安装应用程序的目的。 如果对于您的应用程序,推送通知的传递不是那么关键,那么找出移动Yandex.Mail堆放了哪些自行车仍然很有趣。
它是关于远程通知的,即通过APN(Apple推送通知服务)从服务器发出的通知。 我们不会涉及本地通知,而是谈论:
- 用于推送通知的API是什么样的。 考虑一个推送通知传递方案,以及该方案中可能发生损失的地方。
- 您是如何决定处理Yandex.Mail(推送通知队列)中的损失的。
- 如何记录日志以及其他哪些困难可以解决。
我们拥有什么,我们在哪里失去
现在,用于处理推送通知的API已经足够强大,可以让您做很多有趣的事情。 但这并非总是如此。

以前,推送通知看起来完全像这样-这是一个不幸的蓝色仪表板,它突然在屏幕上弹出,阻止了当前应用程序的工作,不允许进行任何操作,然后永久消失,并且不再有任何提醒。
从那时起已经过去了足够的时间。

对于我们来说,作为开发人员,当推送通知可用于第三方库时,一切
都始于iOS 3 。
通知中心出现在iOS 5中 ,推送通知无处不在,现在它们保留在通知中心中,可以在其中再次查看。
iOS 6推出了“请勿打扰” 。 用户有机会设置他不想接收通知的时间段。
这些更改主要涉及用户如何使用推送通知,如何使生活更加舒适,而不是开发人员如何影响通知。
对于开发人员来说,重要的里程碑是
iOS 8和Notification Action的出现 ,它允许通过推送通知执行特定于特定应用程序的操作。
IOS 10引入了Notification Service Extension和Notification Content Extension 。 第一个允许您在向用户显示推送通知之前对其进行修改。 第二个是通过“推送通知”上的“推送通知”显示一些UI,例如,您可以在其中显示更详细的信息。 在iOS 10中,该用户界面不可点击-您可以观看,无法触摸。
IOS 11引入了“通知隐私设置” 。 现在,用户可以进入设置并指示他是否希望显示传入通知的内容。 这是迈向安全性的重要一步。 仅花了8个版本的iOS即可了解到,并非所有用户都希望个人信息突然出现在桌上的iPhone上。
在iOS 12中,可以通过线程ID
对推送通知进行分组 ,并且我们在iOS 10中收到的带有Notification Content Extension的UI可以单击。 现在,您可以在此处添加按钮和手势控件-所有这些都可以帮助用户与UI进行交互。
今天推送通知
如您所见,推送通知已经走了很长一段路,今天,有了它们的帮助,您确实可以做很多事情。
短信和本地化
和以前一样,我们可以在推送通知中发送文本消息,但是现在您可以另外指定用于本地化的键。
"aps" : { "alert" : { "title" : "New Mail", "subtitle-loc-key" : "alert_subtitle_localization_key", "loc-key" : "alert_body_localization_key", } }
如果在有效负载通知中指定了
subtitle-loc-key
和
loc-key
,则当推送通知到达设备时,将在应用程序的Localizable.string文件中找到必要的值,并且用户将看到本地化的消息。
声音和严重警报
和以前一样,您可以将声音添加到有效负载通知中。
"aps" : { "sound" : { "critical" : 1, "name" : "bingbong.aiff", "volume" : 1.0, } }
IOS 12具有严重警报。 即使用户处于“请勿打扰”模式,这些声音也会播放。
通常,用户在晚上不需要例如订阅杂志的应用程序来报告新号码已发布。 因此,Apple限制了可以使用严重警报的应用程序。 如果您的应用程序具有健康,安全性,或者您认为严重警报确实可以真正帮助用户与应用程序进行交互,请写信给Apple。 也许他们将允许您使用此功能。
静默通知
用户看不到静默通知。 它们直接进入应用程序,将其唤醒,并允许您执行一些操作以使应用程序保持最新状态:向服务器发送请求,在后台请求数据,从数据库更新数据,更新UI,以便在用户进入应用程序时看到更新的数据。
"aps" : { "content-available" : 1
为了使推送通知变为静默,您必须在有效负载中指定:
"content-available" : 1
。 并且不要在有效负载中指定警报,声音和徽标键-它们对于不会显示给用户的推送通知完全没有用。
通知分组
要对消息进行分组,必须在有效负载中指定“ thread-id”。 如果您想以不同的方式进行分组,则可以在同一应用程序中包含多个值:按帐户,按收件人,按主题。
"aps" : { "thread-id" : "any_thread_identifier" }
这非常方便,因为现在推送通知不会占用锁定屏幕上的所有空间,而是将它们组合在一起。 如果您尚未使用此功能,那么该开始了。
在显示之前更改通知
推送通知可以在显示之前进行更改。 为此,您需要将Notification Content Extension添加到应用程序并覆盖
didReceive
方法。 通过这种方法,您可以获取并修改通知内容。
"aps" : { "mutable-content" : 1 } override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { guard let mutableContent = request.content.mutableCopy() as? UNMutableNotificationContent else { contentHandler(request.content); return } mutableContent.subtitle = "Got it!" contentHandler(mutableContent) }
例如,您可以在通知中发送指向媒体内容的链接,在Extension中下载内容,并将下载的内容附加到通知中。 之后,使用新的上下文调用完成功能,并向用户显示扩展的推送通知。 您可以更改标题,副标题等。
另一个有趣的情况是,如果您想要进一步保护数据,而Apple没有看到,则可以发送带有加密上下文的推送通知。 在Notification Content Extension中,您可以解密它们并向用户显示已经解密的数据。
隐藏的通知内容
在iOS 11中,可以隐藏推送通知的内容,而我们作为开发人员不能以任何方式影响这一点。 如果用户勾选了“隐藏通知内容”,则将其隐藏。 我们所能做的就是通过UNNotificationCategory指定将显示的占位符而不是内容(默认为通知),并设置显示标题还是副标题。
let commentCategory = UNNotificationCategory(identifier: "comment-category", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: NSString.localizedUserNotificationString(forKey:"COMMENT_KEY",arguments: nil), options: [.hiddenPreviewsShowTitle])
通知步骤而无需启动应用程序
为了在不启动应用程序本身的情况下执行推送通知操作,您需要创建一个类别并向其中添加操作。 类别标识符传递到有效负载通知的类别字段。 您可以将不同的操作连接到不同类型的通知。
"aps" : { "category" : "message" } let action = UNNotificationAction(identifier:"reply", title:"Reply", options:[]) let category = UNNotificationCategory(identifier: "message", actions: [action], minimalActions: [action], intentIdentifiers: [], options: []) UNUserNotificationCenter.current().setNotificationCategories([category])
丰富的通知
在此扩展中,您可以处理添加到推送通知中的其他操作并显示自定义UI。
为此,您需要向应用程序中添加Notification Content Extension,在其中定义一个继承自UNNotificationContentExtension的类,然后像常规UIViewController一样使用它。
class NotificationViewController: UIViewController, UNNotificationContentExtension { @IBOutlet var userLabel: UILabel? func didReceive(_ notification: UNNotification) { let content = notification.request.content self.title = content.title let userInfo = content.userInfo self.userLabel?.text = userInfo["video-user"] as? String } }
如果要处理自定义操作,请务必记住这些操作值得更新显示给用户的UI。 无需尝试在此扩展中实现业务逻辑。 在主应用程序中通过带推送通知的操作将请求发送到服务器,而不是在此处。 这个地方仅用于UI。
推送通知传递方案
查看您可以在iOS中使用推送通知执行多少操作。 从一个版本到另一个版本,我们拥有越来越多的新功能,但是现在,推送通知传递方案与iOS 3中的方案完全相同。

有人会认为从一开始就推送通知传递方案是可以的,但事实并非如此。
推送通知传递方案中有三个主要节点:
- 产生有效载荷推送通知的提供者;
- APN-Apple推送通知服务,用于发送通知;
- iOS设备和您的应用程序。
我将跳过有关如何注册,接收令牌以及将令牌发送到哪里的部分。 假设我们拥有了所有这些。 接下来会发生什么?
- 提供程序生成有效负载并将其发送到APN。
- APNs将其发送到设备。
- 用户在他的设备上看到推送消息。
邮件和许多其他应用程序使用高级的推送通知传递方案。 添加了Notification Service Extension,它接收带有
"mutable-content" : 1
推送通知
"mutable-content" : 1
。 提供者分为处理应用程序后端逻辑的服务器和提供者本身,后者生成有效负载并处理订阅。
在Yandex中,形成有效负载的提供程序称为XIVA。 XIVA是订阅数据库。 Mail使用XIVA作为第三方库处理推送通知。
在Mail中,订阅的工作非常平常。 我们不仅为通知书签名,而且还有多个帐户。 我们可以签署不同的帐户,或者在一个帐户中选择用户要接收通知的文件夹以及不希望接收通知的文件夹。 XIVA处理所有这一切。 Yandex的其他一些服务也可以通过XIVA使用:有关应用程序,通知,订阅,令牌的所有信息都存储在XIVA中。
损失在哪里?
推送通知传递方案中有四个箭头;在其中的三个转换中可能会发生损失。
在以下情况下
,服务器与XIVA之间可能会发生损失。 用户收到了一封信,服务器知道了这封信,生成了一个通知并将其发送给XIVA。 但是XIVA可能会丢失此信息,例如,如果应用程序中的用户处于脱机状态时选择“订阅”到特定文件夹。 这样,XIVA将不会收到有关该文件夹订阅的信息,并且当有效负载到达时,它将被简单地删除,并且用户将不会看到该通知。
在XIVA和APN之间 ,可能会发生网络丢失。 我们几乎不会影响网络,因此我们将不再赘述。
如果不使用扩展名
,则在APNs和Extension之间,或者在APNS和iOS之间 。 这是最常见的损失类型。 之所以会发生此类损失,是因为APN在设备上每个应用程序存储的推送次数不超过一个。 如果在用户离线时他收到多个通知,那么当他在线时,他只会看到最后一条消息。
这些损失是相同的,因此我们无法保证交付并依赖推送通知。 苹果明确指出,不能保证交货。
在Extension应用程序和iOS之间, 不会发生 损失 ,Apple对此予以保证。 如果您使用Extension并使用完成方法覆盖didReceiveContent,即使您不调用此完成方法,该通知也将始终显示。 要记住这一点很重要。 您可能没有调用它,或者没有时间调用它,但是该通知将以来自APN的形式显示而没有任何更改。
我们将研究如何处理APN和Extension之间的损失。 但是,如果您需要提高推送通知的可传递性,请查看整个方案。 检查服务端是否有任何损失,您的提供商是否与APN正常交互等等。 检查并测量整个链,然后得出结论,即损耗最容易发生的位置以及应修改电路的哪一部分。
推送通知队列
我们处理APN和扩展包捆绑中的损失的方法称为“推送通知队列”。
如果将整个故事压缩为一个短语,那么它将是:
如果错过了推送通知,则可以再次请求。

在我们的通知传递方案中,所有相同的参与者是:XIVA,APN,扩展。 简化方案如下所示:
- XIVA将要发送给APN的推送通知编号,然后才发送信息。
- Extension收到一个1的推送通知,一段时间后收到3的推送通知。它知道某些数据丢失。
- 向XIVA发送最后一个接收到的请求diff,并要求再次发送丢失的数据。
- XIVA重新发送推送通知,因为它存储有效负载数据库和订阅数据库。 所有订阅都会存储一段时间,并且可以重新请求。
- 我们重新询问,收到推送通知,并且在客户端上拥有客户端应该收到的所有消息。
预期的第一个问题是重复的通知。 当我们从XIVA重新请求消息时,我们不知道发送队列中有什么消息,因为我们不是直接通过APN与之通信。 假设我们发现缺少一些通知,并向XIVA发送了一个请求。 XIVA通过有效负载APN发送,并且缺少通知。 但是,在我们收到它之前,我们还收到了另一个有效载荷,并且还带有通行证。 他们再次询问-XIVA又发送了。
为了避免重复通知,我们使用
apns-collapse-id 。 此设置允许iOS端折叠具有相同ID的推送通知。 如果具有相同apns-collapse-id的多个推送通知已到达设备,则iOS将折叠它们,并且用户将仅看到一个通知。
XIVA
我将告诉您这一切在XIVA上是如何工作的,因为总是很好奇后端会发生什么。
XIVA在推送通知队列之前存在,并且是订阅数据库。 重要的是所有内容都由用户存储在数据库中:
- 密钥是
<service, user>
。 - 有效负载存储为值(在Mail情况下,有关字母的数据)。
XIVA从数据库中获取数据并发送到APN或其他服务,因为它不仅适用于iOS。 我们决定重用它。
我们来到了XIVA开发团队,并真正要求推送通知队列。 原则上,XIVA已经具备了所有功能:数据库,有效负载的TTL,即不会立即删除它们,而是可以转发它们。 唯一缺少的是,可以将推送通知队列配置为当前XIVA实现的一部分-通过编号。
对于传递编号,推送通知应按设备和app_name进行编号。 也就是说,特定设备和特定应用程序需要端到端编号,以便在客户端依赖它。 我们这样做如下:重用XIVA数据库,但是开始使用不同的密钥向其写入有效负载。 现在,apns_queue充当服务,
device_id + app_name
充当用户-需要在客户端上编号的数据,即
key: <apns_queue, device_id + app_name>
。
现在,XIVA从主数据库中获取数据,并在需要发送数据时将其放入队列中。 此时,有效负载将获得一个新的编号,因为现在它们位于同一数据库中,但是具有不同的密钥。 XIVA已经从那里将其取出并通过APN发送。 总共,客户端会收到必要的有效负载编号。
客户端使用Notification Service Extension。
public override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
我们在
didReceive
重新定义了
didReceive
方法,并查看来自服务器的内容。 我们在所有推送通知中添加
"mutable-content" : 1
,以便它们属于扩展,因为否则我们无法在计算中将它们考虑在内。
在该方法内部的代码中,还进行了连续检查:是否有必要的有效负载出现,他们是否可以解析它。 如果未解析,则此消息不是来自XIVA。 如果该消息不是来自XIVA,我们将无法继续使用它,而仅通过APN发出的通知呼叫完成,我们将不进行任何计算。
guard let payload = try? self.payloadParser.parsePayload(from: request.content.userInfo) else {
我们登录,检查deviceId是否已更改,因为我们知道在iOS中是可能的。 老实说,我们没有遇到deviceId的更改,但是以防万一我们正在处理它,因为如果更改,我们将无法信任XIVA的数字。
self.logger.logNotificationReceived(with: payload) if lastPositionDeviceId != deviceId {
进一步看,我们是否可以在此有效负载中接收XIVA数据,无论是否可以接收。 如果不是,请再次调用contentHandler。
guard let xivaInfo = payload.xivaInfo else { contentHandler(request.content); return }
如果有数据,请检查deviceId是否已接收到数据。 XIVA将设备的哈希发送到有效负载,如果它经过验证并匹配,我们继续,否,我们调用contentHandler。
guard isHashCompatible(deviceId: deviceId, deviceIdHash: xivaInfo.deviceIdHash) else {
下一个块是查看是否有保存的位置:
- 如果我们没有上次保存的职位,那么我们要么尚未收到通知,也没有进入分机,要么由于某种原因退出了。 然后没有什么可以推迟的,以找到未找到的差异,我们再次调用完成。
- 如果有,继续前进。
guard let lastPos = lastNotificationPosition else {
我们计算错过的通知数。 如果缺少零是可以的,那么我们什么也没错过。
let missedMessages = xivaInfo.notificationPosition - lastPos - 1 guard missedMessages > 0 else {
否则,我们从XIVA中获取位置数据-来自相同的连续编号。 进一步我们看,遗漏的数量是否不超过某个设定值。
lastNotificationPosition = xivaInfo.notificationPosition guard missedMessages <= repeatMaxCount else {
为什么需要这个? 假设用户已离线很长时间,并且在此期间已累积了一百条消息。 我们将要求全部一百(这对我们来说很容易),XIVA将发送全部一百,并且用户将收到所有通知。 即使我们按thread-id对它们进行分组(并且对它们进行分组),对于每个通知,所有相同,都将调用此Extension,所有检查都将通过。 用户似乎不太可能需要所有一百个通知。 因此,我们会生成一个通知,其中写入以下内容:您有100条未命中的消息,请转到应用程序并进行查找。 而且,我们可以准确地向用户显示此消息,因为我们可以替换推送通知。
所有检查通过后,我们向XIVA发送请求:到达我们的最后一个职位,以及错过的消息数量。 并看:
- 如果XIVA成功答复:“一切正常,我将发送数据”,我们将向用户显示当前通知,并等到XIVA发送其他所有信息,并且用户看到所有丢失的消息。
- 如果XIVA回答错误,则我们向用户显示一条自定义通知,告知他错过了可以在应用程序中查看的消息。
self.requestMissedNotifications(lastPosition: xivaInfo.notificationPosition, gap: missedMessages) { result in result.onValue { _ in self.logger.logNotificationProcessed(with: .success) contentHandler(request.content) }.onError { error in self.logger.logNotificationProcessed(with: .failure(error)) contentHandler(buildNewNotification()) } }
因此,在客户端上的实现归结为大量检查,在检查中我们发现是否可以处理接收到的数据。
测井及其他困难
如您所知,要确保该方法行之有效,您需要登录。 我们开始收集有关传递通知的新方法的统计数据,并比较传递能力的变化。
推送扩展的局限性
我们遇到的第一件事是推送扩展限制。
并不总是叫 。 如果您在应用程序设置中关闭了通知绘图(接收通知的功能保持打开,但所有可能的显示功能都已关闭),则不会调用Extension-带有重新计数的所有逻辑,最重要的是,不会调用日志记录。 我们将无法找出对我们最重要的内容-用户是否已收到通知。
推送扩展有时间限制 。 Apple文档说,您需要在30秒钟之内用修改后的通知呼叫完成,否则将显示初始通知。
我想知道我们是如何解决的。 我们实现了一项称为“美丽”推送通知的功能,将媒体元素附加到通知中,更改了标题,副标题。 在测试过程中,结果发现一些推送通知变得很漂亮,而其余的则仍然是丑小鸭。
我们开始研究这些推送通知之间的差异,发现两者之间没有差异,只是对于某些我们成功完成的呼叫,而对于其他一些则没有。 , , push- , APNs.
— . Apple , , push-extension, , , . , 12 .
Apple Developer Forum , , . , — 10 .
, . AppMetrica. , AppMetrica , Extension . , - .
: Extension .
push-extension UserDefaults. , , AppMetrica.
. . , , . , . , XIVA ( ), , .
, Notification Extension iOS 10 , Extension, , .
AppMetrica : , push-extension . AppMetrica push-, , . ,
AppMetrica Push SDK .
, . — , . , .

— , , .
, push-, , — .
, , . , …
: , , . , ? - , push-? , ? user experience ?
, 2–3–20 ?
, , , , , , , . , push-. , .
总结
Push- iOS . , .. , .
push- ( ) . . XIVA. , , . , , . !
push-extension. , . , .
, . , , , , - . , push- . , , , App Store, , !
AppsConf , 21 22 , .. 50 , . 1 , — .