最近很多人表示升级iOS12.1后原有的播报程序无法正常运行,试了很多方法始终不行。之前需求太忙了,现在终于有时间沉下心来看一下这方面的问题,最后终于找到一个比较合适的解决办法。
其实代码很简单,在这里主要总结一下整个问题解决的思路。没有耐心的小朋友可以直接拉到最后,有解决方法。
一、原因分析:为什么会无法播报
我们之前采用的方法是,在Extension中拿到需要播报的内容,然后用合成语音的工具将文字转换成语音,再在Extension中进行播放。但进入12.1后我们打断点,是可以看到对应的错误提示,无法在后台进行audio的播放。
明显的苹果在12.1上对Extension做了更多的完善和限制。原来基本上可以等同于一个完整的app,现在做的更像是一个挂载的包。虽然这些限制给我们造成了bug,但是这样做还是有好处的,毕竟原来没有限制的Extension是可以做很多你想象不到的事儿的,算是苹果对之前出的功能做了完善和补救。
很多同学一开始抱着等待苹果修复这个问题的心态,在等,觉得这是苹果更新版本的bug。我只能说这些同学太天真了。从文档中可以看出,serviceExtension一开始诞生的目的,是为了给开发者提供一个改变服务器推送给iphone通知内容的程序,比如说对一些敏感内容进行解密,或者根据客户端具体状态改变通知展示内容。你在这里面偷摸的进行后台音频播放,这本身其实就是一个比较鸡贼的做法。
Notification Service Extension官方文档
二、方案制定
既然已经定位到了问题出现的原因,接下来就是着手解决了。由于是之前没有遇到过的问题,说实话需要对整个推送过程重新梳理,看看有什么切入点来解决整个问题。毕竟不能说苹果不允许这个功能就不做了。
实现语音播报功能主要就是有以下三个途径
- Notification Service Extension
- 程序进入后台时保活播放
- 利用VoIP唤醒app,执行代码进行播报
后台播放就不聊了,很早之前就研究过这种方案,能播,但是很不稳定,而且程序被杀死的情况下是无法播报的。
VoIP是可以实现功能,但是首先是审核的问题,如果你没有VoIP的功能,你是很难通过苹果的审核的,毕竟随意唤醒程序这种功能,苹果的给予还是很谨慎的。再有就是整个推送后台,如果换VoIP就需要整个重构。为了保证语音播报的准时送达,及时播报,我们也做了很多努力,我们整个推送架构现在已经相当稳定,整个重构对业务影响是巨大的。
经过分析后,解决问题的关键还是在Notification Service Extension上。因为没有方向,没有前人经验可以借鉴,只能所有可能的方式都试一下。一条路一条路试是很痛苦的,因为你不知道什么时候就撞到了南墙,然后继续迷失方向。但是根据经验,大方向基本是有的,就看哪条路能通了,而且有时候通向真理的路很有可能就是隐藏在林荫间的小路,初极狭,才通人,过后才霍然开朗。
三、着手解决
1.在plist里增加后台播放音频特性
既然你丫不让我后台播放,那我就强行允许自己能后台播放。
我们知道苹果对于app的生命周期管控是很严格的,这也是为什么iOS的性能要比安卓好很多。你想要在后台运行,可以,但必须上报,必须说明你哪些功能要后台运行,如果我不认可,还不给你过(苹果爸爸就是这么牛逼)。
但是当你找到Extension的功能列表时会惊讶的发现,根本就没有勾选的地方。说白了,就是这些功能,苹果一个都不给你在扩展里用。
不给我选我自己手改总可以了吧,找到扩展的info.plist打开源码,添加后台音频播放。
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
运行,走你!这时你会发现,我曹,解决了。一切恢复了正常。我真牛逼。
然后你兴奋的告诉产品,问题解决了,赶紧发个版本,把这个问题修复。然后你就会发现,打包上传appStore时候报错。。说这个字段是非法的,无法添加的。
好吧,你检查我的包,我就写在代码里,让你检测不出来。我就尝试在代码里动态的往info.plist里加代码。很可惜,也不行。在代码里,是无法对info.plist进行任何修改,你有读取的权限,却没有写入的权限。
好吧,这条路放弃!
2.通过Service Extension唤醒主app进行播报
既然扩展你不让播了,我告诉我大哥让他播总可以了吧。既然扩展是另一个程序,我通过openURL让住程序播放。听起来像是个不错的方案。
但写了之后又是各种报错,无法在扩展里用这个类,用那个类,编译都不给过!好吧,这条路放弃!
3.使扩展进入“前台”
既然是后台播放不允许,那我如果在前台你总不能不让我播吧。尝试着寻找一下扩展的生命周期,结果发现依然是无从下手。AppDelegate里面的生命周期的方法,写到扩展里面,根本就不走,所以就更谈不上改变了。
结论就是扩展,只是苹果暴露给开发者的一个执行代码的方法,比之前我认知的一个完整app,在开发者来看还是有很大区别的。
4.修改通知的UNNotificationSound
这时,之前的尝试基本都白费了,陷入了困局。每当到这种时候我就会静下心来,去看苹果官方的文档,看看有没有什么灵感或启发。这时我看到了一个属性。
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
给我们暴露出来的修改推送内容的属性其实是接收到的UNMutableNotificationContent,既然他的title、subTitle可以修改,那可不可以修改他的声音呢。
UNMutableNotificationContent有一个UNNotificationSoundName的sound属性,这个属性就是通知来的时候手机发出的声音,我们可以事先在app的bundle里、或者在Library/Sounds路径下,预置好对应的音频文件就可以播放。
// The name of a sound file to be played for the notification. The sound file must be contained in the app’s bundle or in the Library/Sounds folder of the app’s data container. If files exist in both locations then the file in the app’s data container will be preferred.
+ (instancetype)soundNamed:(UNNotificationSoundName)name __WATCHOS_PROHIBITED;
这样就又有了一个思路
- 在推送扩展里将要播放的音频文件合成之后,存储到Library/Sounds的目录下,然后再修改推送的声音为该文件,就可以播放了。
由于我们之前就是用的合成本地的mp3形式播放的,所有合成和存储是没有难度的。但当我按这样方式执行了之后,却发现依旧不能播。
原因是扩展和主程序是两个不互通的bundle,你在扩展里存进去了,但是推送到达住app之后,他找的确是自己的路径底下的文件。
尝试了很多方法,什么文件共享,修改路径等等等等都不行。
难道这条路也要放弃?
就在这时我灵光一闪,那既然这两个是不同的app那我直接在扩展里,发送本地通知,这样我其实是相当于给扩展发一个通知,这下总该能找到对应的文件了吧。这样就又有了一个思路
- 扩展在收到通知之后 -> 合成音频 -> 存储到扩展的对应路径 -> 扩展自己给自己发一个本地通知那个通知的sound设置成合成文件
自信满满修改代码,走你。结果还是没播,但是由于我偷懒没有改存储的文件名字,意外的发现,我之前存在主程序里的文件被播放了!
等等,为什么我在扩展里发本地通知,却发到了主程序里??难道是我的错觉?
我又在主程序里面放了几个单独的音频文件,发现了原来在扩展里面发本地通知,最后的接收方是主程序!!
这时我已经胸有成竹了,因为我知道这个问题已经被我解决了!
四、最终实现
最终的程序很快就被敲出来了。方法如下
- 将你想要播放的音频拆分,放到主程序的包里
- 利用Service Extension,在收到服务端的推送的时候,按照顺序发送本地通知
- 本地通知的sound就是对应的音频拆分
这个方案和12.1之前的播报效果基本一模一样,而且无需后台改动,可以说是现在播报的最完美替代方案。
但是这个方案也有缺陷,不能完美的动态播放,只能是一些比较套路的文案,相对的动态播放,但这已经解决了我们业务遇到的问题,所以还是比较完美的方案。
最后谢谢大家观看,如果我的文章解决了你的问题,帮忙点个喜欢哦~
有错误和问题也欢迎指正,大家一起交流进步~