iOS13微信收款到賬語音提醒開發總結
- 2020 年 1 月 13 日
- 筆記
隨着WWDC2019後推出了iOS13系統,蘋果已不再允許將PushKit應用在非Voip語音通話的場景上,開發者必須在接入CallKit的情況下才能使用PushKit。有不少同事通過我以前的文章找到我,詢問微信語音播報在iOS13下的解決方案,這篇文章對iOS下微信支付語音播報從Voip方案遷移到Notification Service Extension方案進行了總結。
一、背景
隨着蘋果爸爸在WWDC2019發佈了新的iOS13,兩年前的這篇微信iOS收款到賬語音提醒開發總結方案已經不再適用,具體的原因是iOS13中(準確的說是使用XCode11編譯)蘋果不再允許PushKit應用在非voip電話的場景上。在iOS13中,蘋果比以往更關注用戶的隱私以及設備的電池續航問題,所以對PushKit的能力進行了收攏。如果需要使用PushKit的話則需要接入CallKit的接口,導致收到客戶端收到Voip Push時會拉起一個接打電話的全屏界面,有在國區發佈過應用的同學應該知道拉起這個界面是不被甲方允許的。這篇文章總結了在iOS13下的語音播報遷移方案以及一些需要注意的問題。目前微信的7.0.10版本已經帶上了這部分的特性。
二、技術方案
Notification Service Extension
新的方案是主要是利用了蘋果在iOS10中推出的Notification Service Extension(以下簡稱NSE),當apns的payload上帶上"mutable-content"的值為1時,就會進入NSE的代碼中。與Voip方案最大的不同之處是,NSE不能喚醒主應用,也不能訪問主應用的文件空間,只能在Extension進程中處理相應的邏輯。在NSE中,開發者可以更改通知的內容,利用離線合成或者從後台下載的方式,生成需要播報的內容,通過自定義通知鈴聲的方式,達到語音播報提醒的目的。NSE方案也是蘋果在WWDC2019的Session707上推薦的解決方式。
UNNotificationSound
在NSE中,可以通過給UNNotificationContent中的Sound屬性賦值來達到在通知彈出時播放一段自定義音頻的目的。
// 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.
文檔中明確描述了音頻文件的存儲路徑,以及讀取的優先級:
- 主應用中的Library/Sounds文件夾中
- AppGroups共享目錄中的Library/Sounds文件夾中
- main bundle中
自定義鈴聲支持的聲音格式包括,aiff、wav以及wav格式,鈴聲的長度必須小於30s,否則系統會播放默認的鈴聲。
而且由於是通知鈴聲,聲音是默認跟靜音開關的,不需跟以前一樣再使用判斷靜音開關的黑魔法(黑魔法在不同機型上偶爾會出現誤判的情況)。
AppGroups
由於我們是在NSE中自定義鈴聲,所以1和3這兩個文件路徑我們是無法訪問的。只能將合成好或者下載到語音音頻文件存儲到AppGroups下的Library/Sounds文件夾中,需要在Capablities中打開這個AppGroups的能力,即可通過NSFileManager
的containerURLForSecurityApplicationGroupIdentifier:
方法訪問AppGroups的根目錄。
語音合成
微信的收款到賬語音依賴了我們自研的強大的離線語音合成庫。apns的payload中攜帶了需要合成的文本內容,通過離線語音合成庫生成wav音頻文件後,將文件寫到AppGroups的Library/Sounds文件夾下,最後更改UNNotificationSound屬性即可使通知播報一段自定義的收款到賬語音。
如果一些小型的企業本身不具備有離線合成的能力(看了下市面上的幾個比較厲害的離線合成服務都是需要收費的),則可以採用在線合成再通過http下載的方式,訊飛和微信都有提供免費的服務。這個方案的缺點是依賴後台和當前的網絡環境,有可能會導致消息播報不及時的問題。如果出現30s內都無法現在成功,需要在serviceExtensionTimeWillExpire方法中進行處理,最好的兜底方案是播放一段默認的語音。
三、開發過程中遇到的問題
消息播放隊列
NSE方案有個問題是:當客戶端短時間內收到多條播報通知時,後面的通知會頂掉前面的通知,導致前面的通知播報不完整,這種情況對於商家來說是比較困擾的。所以需要增加一個消息隊列,將所有需要播報的通知都添加到隊列中,當前面的消息播放完畢後,再播放後面的消息。音頻的播放時間可以讓後台通過payload推送,如果是自己的合成的wav可以通過播放時間 =(音頻大小 - 音頻頭)/ (採樣頻率 * 採樣精度 * 通道數)
進行計算。
多線程問題
要注意的是,NSE的代碼邏輯並不是在主線程執行的。蘋果這樣的設計是非常合理的,一方面避免了開發者在NSE由於代碼設計失誤導致前台的其他應用界面卡住的問題,另一方面是主工程此時已被掛起或者已被kill掉,本來也不應該給主線程的執行時間給到NSE。
所以我們在處理上面提到的消息播放隊列,以及涉及到文件讀寫的邏輯上,需要給相應的代碼邏輯加鎖,否則會出現多線程問題。
消息去重
由於支付的消息相較於普通消息對可達性與實時性的要求更高,所以當初設計的時候使用了雙通道來降低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 thedidReceive(_:withContentHandler:)
method in time, the system displays the notification』s original content.
如果你30s內沒調用handler方法,並且沒有實現serviceExtensionTimeWillExpire方法,那麼系統會幫你主動推送後台推給客戶端的原內容。
這裡的解決方案是讓後台,讓雙通道觸發的apns消息在requestheader上帶上同樣的apns-collapse-id,後面的通知就會覆蓋前面的通知。但是這裡還有個問題就是雖然用戶看到的是一條消息,但是聲音還是會播兩次。這裡就可以通過記錄已播放的消息單號,後面再重現重複的單號就講sound設置為一段空白的音頻就ok了。
三、總結
其實現在回頭看,NSE是比Voip更優雅的一個方案,NSE方案的總體代碼量也比Voip少了不少,為什麼當初沒有選擇這個方案呢?這裡其實也有它的歷史原因,一方面是NSE是iOS10以後才出現的新Extension,做第一版方案的時候也是iOS10剛發佈,對其的了解程度也不夠。另一方面,微信當時也不具備離線合成語音的能力的,只能通過Cgi去拉在線合成語音,而微信的Extension當時也不具備Cgi請求的能力。切換到NSE方案後,最好的一個體驗是語音播報與靜音開關能完美契合,另一方面是令人詬病的消息延遲問題也有所改善。
四、相關資料
Advances in App Background Execution – 2019
UNNotificationServiceExtension