??最近很多人表示升級(jí)iOS12.1后原有的播報(bào)程序無(wú)法正常運(yùn)行,試了很多方法始終不行。之前需求太忙了,現(xiàn)在終于有時(shí)間沉下心來(lái)看一下這方面的問(wèn)題,最后終于找到一個(gè)比較合適的解決辦法。
??其實(shí)代碼很簡(jiǎn)單,在這里主要總結(jié)一下整個(gè)問(wèn)題解決的思路。沒(méi)有耐心的小朋友可以直接拉到最后,有解決方法。
一、原因分析:為什么會(huì)無(wú)法播報(bào)
??我們之前采用的方法是,在Extension中拿到需要播報(bào)的內(nèi)容,然后用合成語(yǔ)音的工具將文字轉(zhuǎn)換成語(yǔ)音,再在Extension中進(jìn)行播放。但進(jìn)入12.1后我們打斷點(diǎn),是可以看到對(duì)應(yīng)的錯(cuò)誤提示,無(wú)法在后臺(tái)進(jìn)行audio的播放。
??明顯的蘋(píng)果在12.1上對(duì)Extension做了更多的完善和限制。原來(lái)基本上可以等同于一個(gè)完整的app,現(xiàn)在做的更像是一個(gè)掛載的包。雖然這些限制給我們?cè)斐闪薭ug,但是這樣做還是有好處的,畢竟原來(lái)沒(méi)有限制的Extension是可以做很多你想象不到的事兒的,算是蘋(píng)果對(duì)之前出的功能做了完善和補(bǔ)救。
??很多同學(xué)一開(kāi)始抱著等待蘋(píng)果修復(fù)這個(gè)問(wèn)題的心態(tài),在等,覺(jué)得這是蘋(píng)果更新版本的bug。我只能說(shuō)這些同學(xué)太天真了。從文檔中可以看出,serviceExtension一開(kāi)始誕生的目的,是為了給開(kāi)發(fā)者提供一個(gè)改變服務(wù)器推送給iphone通知內(nèi)容的程序,比如說(shuō)對(duì)一些敏感內(nèi)容進(jìn)行解密,或者根據(jù)客戶(hù)端具體狀態(tài)改變通知展示內(nèi)容。你在這里面偷摸的進(jìn)行后臺(tái)音頻播放,這本身其實(shí)就是一個(gè)比較雞賊的做法。
Notification Service Extension官方文檔
二、方案制定
??既然已經(jīng)定位到了問(wèn)題出現(xiàn)的原因,接下來(lái)就是著手解決了。由于是之前沒(méi)有遇到過(guò)的問(wèn)題,說(shuō)實(shí)話需要對(duì)整個(gè)推送過(guò)程重新梳理,看看有什么切入點(diǎn)來(lái)解決整個(gè)問(wèn)題。畢竟不能說(shuō)蘋(píng)果不允許這個(gè)功能就不做了。

實(shí)現(xiàn)語(yǔ)音播報(bào)功能主要就是有以下三個(gè)途徑
- Notification Service Extension
- 程序進(jìn)入后臺(tái)時(shí)?;畈シ?/li>
- 利用VoIP喚醒a(bǔ)pp,執(zhí)行代碼進(jìn)行播報(bào)
后臺(tái)播放就不聊了,很早之前就研究過(guò)這種方案,能播,但是很不穩(wěn)定,而且程序被殺死的情況下是無(wú)法播報(bào)的。
VoIP是可以實(shí)現(xiàn)功能,但是首先是審核的問(wèn)題,如果你沒(méi)有VoIP的功能,你是很難通過(guò)蘋(píng)果的審核的,畢竟隨意喚醒程序這種功能,蘋(píng)果的給予還是很謹(jǐn)慎的。再有就是整個(gè)推送后臺(tái),如果換VoIP就需要整個(gè)重構(gòu)。為了保證語(yǔ)音播報(bào)的準(zhǔn)時(shí)送達(dá),及時(shí)播報(bào),我們也做了很多努力,我們整個(gè)推送架構(gòu)現(xiàn)在已經(jīng)相當(dāng)穩(wěn)定,整個(gè)重構(gòu)對(duì)業(yè)務(wù)影響是巨大的。
經(jīng)過(guò)分析后,解決問(wèn)題的關(guān)鍵還是在Notification Service Extension上。因?yàn)闆](méi)有方向,沒(méi)有前人經(jīng)驗(yàn)可以借鑒,只能所有可能的方式都試一下。一條路一條路試是很痛苦的,因?yàn)槟悴恢朗裁磿r(shí)候就撞到了南墻,然后繼續(xù)迷失方向。但是根據(jù)經(jīng)驗(yàn),大方向基本是有的,就看哪條路能通了,而且有時(shí)候通向真理的路很有可能就是隱藏在林蔭間的小路,初極狹,才通人,過(guò)后才霍然開(kāi)朗。
三、著手解決
1.在plist里增加后臺(tái)播放音頻特性
既然你丫不讓我后臺(tái)播放,那我就強(qiáng)行允許自己能后臺(tái)播放。

我們知道蘋(píng)果對(duì)于app的生命周期管控是很?chē)?yán)格的,這也是為什么iOS的性能要比安卓好很多。你想要在后臺(tái)運(yùn)行,可以,但必須上報(bào),必須說(shuō)明你哪些功能要后臺(tái)運(yùn)行,如果我不認(rèn)可,還不給你過(guò)(蘋(píng)果爸爸就是這么牛逼)。
但是當(dāng)你找到Extension的功能列表時(shí)會(huì)驚訝的發(fā)現(xiàn),根本就沒(méi)有勾選的地方。說(shuō)白了,就是這些功能,蘋(píng)果一個(gè)都不給你在擴(kuò)展里用。
不給我選我自己手改總可以了吧,找到擴(kuò)展的info.plist打開(kāi)源碼,添加后臺(tái)音頻播放。
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
運(yùn)行,走你!這時(shí)你會(huì)發(fā)現(xiàn),我曹,解決了。一切恢復(fù)了正常。我真牛逼。
然后你興奮的告訴產(chǎn)品,問(wèn)題解決了,趕緊發(fā)個(gè)版本,把這個(gè)問(wèn)題修復(fù)。然后你就會(huì)發(fā)現(xiàn),打包上傳appStore時(shí)候報(bào)錯(cuò)。。說(shuō)這個(gè)字段是非法的,無(wú)法添加的。
好吧,你檢查我的包,我就寫(xiě)在代碼里,讓你檢測(cè)不出來(lái)。我就嘗試在代碼里動(dòng)態(tài)的往info.plist里加代碼。很可惜,也不行。在代碼里,是無(wú)法對(duì)info.plist進(jìn)行任何修改,你有讀取的權(quán)限,卻沒(méi)有寫(xiě)入的權(quán)限。
好吧,這條路放棄!
2.通過(guò)Service Extension喚醒主app進(jìn)行播報(bào)
既然擴(kuò)展你不讓播了,我告訴我大哥讓他播總可以了吧。既然擴(kuò)展是另一個(gè)程序,我通過(guò)openURL讓住程序播放。聽(tīng)起來(lái)像是個(gè)不錯(cuò)的方案。
但寫(xiě)了之后又是各種報(bào)錯(cuò),無(wú)法在擴(kuò)展里用這個(gè)類(lèi),用那個(gè)類(lèi),編譯都不給過(guò)!好吧,這條路放棄!
3.使擴(kuò)展進(jìn)入“前臺(tái)”
既然是后臺(tái)播放不允許,那我如果在前臺(tái)你總不能不讓我播吧。嘗試著尋找一下擴(kuò)展的生命周期,結(jié)果發(fā)現(xiàn)依然是無(wú)從下手。AppDelegate里面的生命周期的方法,寫(xiě)到擴(kuò)展里面,根本就不走,所以就更談不上改變了。
結(jié)論就是擴(kuò)展,只是蘋(píng)果暴露給開(kāi)發(fā)者的一個(gè)執(zhí)行代碼的方法,比之前我認(rèn)知的一個(gè)完整app,在開(kāi)發(fā)者來(lái)看還是有很大區(qū)別的。
4.修改通知的UNNotificationSound
這時(shí),之前的嘗試基本都白費(fèi)了,陷入了困局。每當(dāng)?shù)竭@種時(shí)候我就會(huì)靜下心來(lái),去看蘋(píng)果官方的文檔,看看有沒(méi)有什么靈感或啟發(fā)。這時(shí)我看到了一個(gè)屬性。
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
給我們暴露出來(lái)的修改推送內(nèi)容的屬性其實(shí)是接收到的UNMutableNotificationContent,既然他的title、subTitle可以修改,那可不可以修改他的聲音呢。
UNMutableNotificationContent有一個(gè)UNNotificationSoundName的sound屬性,這個(gè)屬性就是通知來(lái)的時(shí)候手機(jī)發(fā)出的聲音,我們可以事先在app的bundle里、或者在Library/Sounds路徑下,預(yù)置好對(duì)應(yīng)的音頻文件就可以播放。
// 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;
這樣就又有了一個(gè)思路
- 在推送擴(kuò)展里將要播放的音頻文件合成之后,存儲(chǔ)到Library/Sounds的目錄下,然后再修改推送的聲音為該文件,就可以播放了。
由于我們之前就是用的合成本地的mp3形式播放的,所有合成和存儲(chǔ)是沒(méi)有難度的。但當(dāng)我按這樣方式執(zhí)行了之后,卻發(fā)現(xiàn)依舊不能播。
原因是擴(kuò)展和主程序是兩個(gè)不互通的bundle,你在擴(kuò)展里存進(jìn)去了,但是推送到達(dá)住app之后,他找的確是自己的路徑底下的文件。
嘗試了很多方法,什么文件共享,修改路徑等等等等都不行。
難道這條路也要放棄?
就在這時(shí)我靈光一閃,那既然這兩個(gè)是不同的app那我直接在擴(kuò)展里,發(fā)送本地通知,這樣我其實(shí)是相當(dāng)于給擴(kuò)展發(fā)一個(gè)通知,這下總該能找到對(duì)應(yīng)的文件了吧。這樣就又有了一個(gè)思路
- 擴(kuò)展在收到通知之后 -> 合成音頻 -> 存儲(chǔ)到擴(kuò)展的對(duì)應(yīng)路徑 -> 擴(kuò)展自己給自己發(fā)一個(gè)本地通知那個(gè)通知的sound設(shè)置成合成文件
自信滿滿修改代碼,走你。結(jié)果還是沒(méi)播,但是由于我偷懶沒(méi)有改存儲(chǔ)的文件名字,意外的發(fā)現(xiàn),我之前存在主程序里的文件被播放了!
等等,為什么我在擴(kuò)展里發(fā)本地通知,卻發(fā)到了主程序里??難道是我的錯(cuò)覺(jué)?
我又在主程序里面放了幾個(gè)單獨(dú)的音頻文件,發(fā)現(xiàn)了原來(lái)在擴(kuò)展里面發(fā)本地通知,最后的接收方是主程序??!
這時(shí)我已經(jīng)胸有成竹了,因?yàn)槲抑肋@個(gè)問(wèn)題已經(jīng)被我解決了!
四、最終實(shí)現(xiàn)
最終的程序很快就被敲出來(lái)了。方法如下
- 將你想要播放的音頻拆分,放到主程序的包里
- 利用Service Extension,在收到服務(wù)端的推送的時(shí)候,按照順序發(fā)送本地通知
- 本地通知的sound就是對(duì)應(yīng)的音頻拆分
這個(gè)方案和12.1之前的播報(bào)效果基本一模一樣,而且無(wú)需后臺(tái)改動(dòng),可以說(shuō)是現(xiàn)在播報(bào)的最完美替代方案。
但是這個(gè)方案也有缺陷,不能完美的動(dòng)態(tài)播放,只能是一些比較套路的文案,相對(duì)的動(dòng)態(tài)播放,但這已經(jīng)解決了我們業(yè)務(wù)遇到的問(wèn)題,所以還是比較完美的方案。
最后謝謝大家觀看,如果我的文章解決了你的問(wèn)題,幫忙點(diǎn)個(gè)喜歡哦~
有錯(cuò)誤和問(wèn)題也歡迎指正,大家一起交流進(jìn)步~