一、背景
2018年,受通信監(jiān)管要求,蘋果國(guó)區(qū)AppStore不允許app支持CallKit。
2019年,隨著iOS 13正式開放, Xcode11隨之發(fā)布,蘋果已不再允許將PushKit應(yīng)用在非VoIP語(yǔ)音通話的場(chǎng)景上,開發(fā)者必須在接入CallKit的情況下才能使用PushKit。也就是說,通過 Xcode 11 編譯的 iOS 13 系統(tǒng) App,如果使用 PushKit 則必須依賴蘋果 CallKit 才能正常接收VoIP推送。
結(jié)論:使用VoIP功能則無法在中國(guó)區(qū)App Store上架。
二、技術(shù)方案
VoIP
以微聊音視頻鈴聲播放為例,在之前的方案中,微聊音視頻消息會(huì)使用VoIP Push Notification,客戶端在被喚醒之后將獲得30s的后臺(tái)運(yùn)行時(shí)間,在這30s內(nèi),微聊被喚醒調(diào)起音視頻頁(yè)面,并播放定制鈴聲音頻。
Notification Service Extension
新的方案是主要是利用了蘋果在iOS10中推出的Notification Service Extension(以下簡(jiǎn)稱NSE),當(dāng)apns的payload上帶上"mutable-content"的值為1時(shí),就會(huì)進(jìn)入NSE的代碼中。與Voip方案最大的不同之處是,NSE不能喚醒主應(yīng)用,也不能訪問主應(yīng)用的文件空間,只能在Extension進(jìn)程中處理相應(yīng)的邏輯。在NSE中,開發(fā)者可以更改通知的內(nèi)容,利用離線合成或者從后臺(tái)下載的方式,生成需要播報(bào)的內(nèi)容,通過自定義通知鈴聲的方式,達(dá)到語(yǔ)音播報(bào)提醒的目的。NSE方案也是蘋果在WWDC2019的Session707上推薦的解決方式。

UNNotificationSound
在NSE中,可以通過給UNNotificationContent中的Sound屬性賦值來達(dá)到在通知彈出時(shí)播放一段自定義音頻的目的。
// 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.
文檔中明確描述了音頻文件的存儲(chǔ)路徑,以及讀取的優(yōu)先級(jí):
1.主應(yīng)用中的Library/Sounds文件夾中
2.AppGroups共享目錄中的Library/Sounds文件夾中
3.main bundle中
自定義鈴聲支持的聲音格式包括,aiff、wav以及caf格式,鈴聲的長(zhǎng)度必須小于30s,否則系統(tǒng)會(huì)播放默認(rèn)的鈴聲。
鎖屏情況下,鈴聲可以持續(xù)30s(與NES強(qiáng)制結(jié)束時(shí)間一致)在亮屏情況下鈴聲只能持續(xù)5s,是系統(tǒng)行為限制。
而且由于是通知鈴聲,聲音是默認(rèn)跟靜音開關(guān)的。
AppGroups
由于我們是在NSE中自定義鈴聲,所以1和3這兩個(gè)文件路徑我們是無法訪問的。只能將合成好或者下載到語(yǔ)音音頻文件存儲(chǔ)到AppGroups下的Library/Sounds文件夾中,需要在Capablities中打開這個(gè)AppGroups的能力,即可通過NSFileManager的containerURLForSecurityApplicationGroupIdentifier:方法訪問AppGroups的根目錄。
示例代碼:
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *containerUrl = [fileManager containerURLForSecurityApplicationGroupIdentifier:@"group.com.wchat.share"];
NSString *newPath = [containerUrl.path stringByAppendingPathComponent:@"Library/Sounds"];
// 清理本地緩存,刪除sounds文件夾(可選)
[fileManager removeItemAtPath:newPath error:nil];
if (![fileManager fileExistsAtPath:newPath]) {
[fileManager createDirectoryAtPath:newPath withIntermediateDirectories:NO attributes:nil error:nil];
}
NSURL *localURL = [NSURL fileURLWithPath:[newPath stringByAppendingPathComponent:[NSString stringWithFormat:@"/%@.%@", identifier, fileType]]];
[fileManager moveItemAtURL:temporaryFileLocation toURL:localURL error:&error];
//設(shè)置播發(fā)音頻 文件名sname.sfileType
self.bestAttemptContent.sound = [UNNotificationSound soundNamed:[NSString stringWithFormat:@"%@.%@", sname, sfileType]];
三、開發(fā)過程中遇到的問題
消息播放隊(duì)列
NSE方案有個(gè)問題是:當(dāng)客戶端短時(shí)間內(nèi)收到多條播報(bào)通知時(shí),后面的通知會(huì)頂?shù)羟懊娴耐ㄖ?,?dǎo)致前面的通知播報(bào)不完整。所以需要增加一個(gè)消息隊(duì)列,將所有需要播報(bào)的通知都添加到隊(duì)列中,當(dāng)前面的消息播放完畢后,再播放后面的消息。
多線程問題
要注意的是,NSE的代碼邏輯并不是在主App工程中執(zhí)行的。一方面避免了開發(fā)者在NSE由于代碼設(shè)計(jì)失誤導(dǎo)致前臺(tái)的其他應(yīng)用界面卡住的問題,另一方面是主工程此時(shí)已被掛起或者已被kill掉,NSE本質(zhì)是push部分而不是調(diào)起主App。
所以我們?cè)谔幚砩厦嫣岬降南⒉シ抨?duì)列,以及涉及到文件讀寫的邏輯上,需要給相應(yīng)的代碼邏輯加鎖,否則會(huì)出現(xiàn)多線程問題。
四、NSE擴(kuò)展
iOS Notification Service Extension 共享空間數(shù)據(jù)互通測(cè)試
五、相關(guān)資料
Advances in App Background Execution - 2019
UNNotificationServiceExtension