iOS13微信收款到帐语音提醒开发总结

以前承诺过发一篇iOS13下微信收款到帐语音提醒的总结文章,一直拖着没有写。一方面是尚未上线,另外一方面是后面一直在搞红包项目,如今两个项目都顺利上线,终于能够停下来为你们总结一下具体的方案和开发中遇到的问题。微信

1、背景

随着苹果爸爸在WWDC2019发布了新的iOS13,两年前的这篇微信iOS收款到帐语音提醒开发总结方案已经不能再使用了,具体的缘由是iOS13中(准确的说是使用XCode11编译)苹果爸爸再也不容许voip push应用在非voip电话的功能,若是须要使用pushkit的话须要接入callkit的接口,致使收到Voip Push会拉起一个接打电话的全屏界面,有在国区发布过应用的同窗应该知道拉起这个界面是不容许的。这篇文章总结了在iOS13下的语音播报迁移方案以及一些须要注意的问题,目前微信的7.0.10版本已经带上了这部分的特性。网络

2、技术方案

Notification Service Extension

新的方案是主要是利用了Notification Service Extension(如下简称NSE),当apns的payload上带上"mutable-content"的值为1时,就会进入NSE的代码中,咱们能够在这里这里去改变后台推送的通知内容,包括通知的铃声。与Voip方案最大的不一样之处是,NSE不能唤醒主应用,也不能访问主应用的文件空间,只能在Extension进程中处理相应的逻辑。app

UNNotificationSound

在NSE中,能够经过给UNNotificationContent中的Sound属性赋值来达到在通知弹出时播放一段自定义音频的目的。ui

// The sound file to be played for the notification. The sound must be in the Library/Sounds folder of the app's data container or the Library/Sounds folder of an app group data container. If the file is not found in a container, the system will look in the app's bundle.this

文档中明确描述了音频文件的存储路径,以及读取的优先级:设计

  1. 主应用中的Library/Sounds文件夹中
  2. AppGroups共享目录中的Library/Sounds文件夹中
  3. main bundle中

自定义铃声支持的声音格式包括,aiff、wav以及wav格式,铃声的长度必须小于30s,不然系统会播放默认的铃声。code

并且因为是通知铃声,声音是默认跟静音开关的,不需跟之前同样再使用判断静音开关的黑魔法(黑魔法在不一样机型上偶尔会出现误判的状况)。接口

AppGroups

因为咱们是在NSE中自定义铃声,因此1和3这两个文件路径咱们是没法访问的。只能将合成好或者下载到语音音频文件存储到AppGroups下的Library/Sounds文件夹中,须要在Capablities中打开这个AppGroups的能力,便可经过NSFileManagercontainerURLForSecurityApplicationGroupIdentifier:方法访问AppGroups的根目录。队列

语音合成

微信的收款到帐语音依赖了咱们自研的强大的离线语音合成库。apns的payload中携带了须要合成的文本内容,经过离线语音合成库生成wav音频文件后,将文件写到AppGroups的Library/Sounds文件夹下,最后更改UNNotificationSound属性便可使通知播报一段自定义的收款到帐语音。进程

若是一些小型的企业自己不具有有离线合成的能力(看了下市面上的几个比较厉害的离线合成服务都是须要收费的),则能够采用在线合成再经过http下载的方式,讯飞和微信都有提供免费的服务。这个方案的缺点是依赖后台和当前的网络环境,有可能会致使消息播报不及时的问题。若是出现30s内都没法如今成功,须要在serviceExtensionTimeWillExpire方法中进行处理,最好的兜底方案是播放一段默认的语音。

3、开发过程当中遇到的问题

消息播放队列

NSE方案有个问题是:当客户端短期内收到多条播报通知时,后面的通知会顶掉前面的通知,致使前面的通知播报不完整,这种状况对于商家来讲是比较困扰的。因此须要增长一个消息队列,将全部须要播报的通知都添加到队列中,当前面的消息播放完毕后,再播放后面的消息。音频的播放时间可让后台经过payload推送,若是是本身的合成的wav能够经过播放时间 =(音频大小 - 音频头)/ (采样频率 * 采样精度 * 通道数)进行计算。

消息去重

因为支付的消息相较于普通消息对可达性与实时性的要求更高,因此当初设计的时候使用了双通道来下降Voip的偶现的丢消息和延迟的问题。以前的Voip方案是客户端会收到两条消息同样的Voip消息,经过记录payload中的单号来对消息进行去重。可是在NSE中,客户端是没法作到主动去重的,根本缘由是NSE的设计理念只是为了修改NotificationContent的内容,而不能阻止通知弹出,这一点能够从超时处理方法的文档中看出:

If your didReceive(_:withContentHandler:) method takes to long to execute its completion block, the system calls this method on a separate thread to give you one last chance to execute the block. Use this method to execute the block as quickly as possible. Doing so might mean providing some fallback content. For example, if your extension is still downloading an image file with the intent of attaching it to the notification’s content, you might update the notification’s alert text to indicate that an image is being downloaded. If you fail to execute the completion block from the didReceive(_:withContentHandler:) method in time, the system displays the notification’s original content.

若是你30s内没调用handler方法,而且没有实现serviceExtensionTimeWillExpire方法,那么系统会帮你主动推送后台推给客户端的原内容。

这里的解决方案是让后台,让双通道触发的apns消息在requestheader上带上一样的apns-collapse-id,后面的通知就会覆盖前面的通知。可是这里还有个问题就是虽然用户看到的是一条消息,可是声音仍是会播两次。这里就能够经过记录已播放的消息单号,后面再重现重复的单号就讲sound设置为一段空白的音频就ok了。

3、总结

其实如今回头看,NSE是比Voip更优雅的一个方案,NSE方案的整体代码量也比Voip少了很多,为何当初没有选择这个方案呢?这里其实也有它的历史缘由,一方面是NSE是iOS10之后才出现的新Extension,作初版方案的时候也是iOS10刚发布,对其的了解程度也不够。另外一方面,微信当时也不具有离线合成语音的能力的,只能经过Cgi去拉在线合成语音,而微信的Extension当时也没有Cgi请求的能力。切换到NSE方案后,最好的一个体验是语音播报与静音开关能完美契合,另外一方面是使人诟病的消息延迟问题也有所改善。