iOS10 語音播報填坑詳解(解決串行播報中斷問題)
在來聊這類需求的解決方案之前,咱們還是先來聊一聊這類需求的真實使用場景:語音播報间螟。語音播報需求運用最為廣泛的應該是收銀對賬了吴旋,就類似于支付寶、微信厢破、收錢吧等的收款語音提示一樣荣瑟。在iOS 10 之前,蘋果沒有提供通知擴展類的時候摩泪,如果想要實現(xiàn)殺進程也可以正常播報語音消息很難笆焰,從ios 10添加了這一個通知擴展類后,實現(xiàn)殺進程播報語音就相對簡單很多了见坑。
我們先來看一個陌生的Tagget
- Notification Service Extension
這個Notification Service Extension 就是蘋果在 iOS 10的新系統(tǒng)中為我們添加的新特性嚷掠,這個新特性就能幫助我們用來解決殺死進程正常語音播報
蘋果官方解釋:UNNotificationServiceExtension
詳細步驟
- 創(chuàng)建一個通知擴展類
- 添加語音播報邏輯代碼
- 設置支持后臺播放
- iOS10 以下系統(tǒng)如何實現(xiàn)串行播報
創(chuàng)建一個通知擴展類
- 首先我點擊 Xcode 的 File -> New -> Target -> Notification Service Extension,新建一個通知擴展類Target鳄梅。
新建完后叠国,我們的工程會多出一個文件夾,這里示例Demo的Target命名為 NotificationSE戴尸,文件夾中有NotificationService.h NotificationService.m 文件粟焊,這兩個文件就是后面我們要用到的通知擴展類文件
在沒有對NotificationService做任何修改時,我們先來預覽下 .m 文件中都有哪些內(nèi)容
從上面的截圖孙蒙,我們可以看到项棠,.m 文件其實很簡單,就 2 個函數(shù)挎峦,其實后面我們對這個文件做邏輯處理香追,也是很簡單的。
添加語音播報邏輯代碼
- 注意坦胶,這里我們使用的語音合成和播報組件也是蘋果官方提供的組件透典,
AVSpeechSynthesizer
晴楔,AVSpeechSynthesisVoice
,AVSpeechUtterance
我們先來看下一段語音播放代碼片段:
AVSpeechSynthesizer *av = [[AVSpeechSynthesizer alloc] init];
AVSpeechSynthesisVoice *voice = [AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"];
AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:@"我是測試文案"];
utterance.rate = 0.5;
utterance.voice= voice;
[av speakUtterance:utterance];
現(xiàn)在我們將 NotificationService .m 文件做修改峭咒,使之支持語音播報税弃。并且能支持多條通知同時過來的串行播報。完整文件如下:
//
// NotificationService.m
// NotificationSE
//
// Created by 劉光強 on 2018/9/17.
// Copyright ? 2018年 quangqiang. All rights reserved.
//
#import "NotificationService.h"
#import <MediaPlayer/MediaPlayer.h>
#import <AVFoundation/AVFoundation.h>
@interface NotificationService ()<AVSpeechSynthesizerDelegate>
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@property (nonatomic, strong) AVSpeechSynthesisVoice *synthesisVoice;
@property (nonatomic, strong) AVSpeechSynthesizer *synthesizer;
@end
@implementation NotificationService
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
// 這個info 內(nèi)容就是通知信息攜帶的數(shù)據(jù)凑队,后面我們?nèi)≌Z音播報的文案则果,通知欄的title,以及通知內(nèi)容都是從這個info字段中獲取
NSDictionary *info = self.bestAttemptContent.userInfo;
// 播報語音
[self playVoiceWithContent: info[@"content"]];
// 這行代碼需要注釋漩氨,當我們想解決當同時推送了多條消息西壮,這時我們想多條消息一條一條的挨個播報,我們就需要將此行代碼注釋
// self.contentHandler(self.bestAttemptContent);
}
- (void)playVoiceWithContent:(NSString *)content {
AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:content];
utterance.rate = 0.5;
utterance.voice = self.synthesisVoice;
[self.synthesizer speakUtterance:utterance];
}
// 新增語音播放代理函數(shù)叫惊,在語音播報完成的代理函數(shù)中款青,我們添加下面的一行代碼
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance {
self.contentHandler(self.bestAttemptContent);
}
- (void)serviceExtensionTimeWillExpire {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
self.contentHandler(self.bestAttemptContent);
}
- (AVSpeechSynthesisVoice *)synthesisVoice {
if (!_synthesisVoice) {
_synthesisVoice = [AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"];
}
return _synthesisVoice;
}
- (AVSpeechSynthesizer *)synthesizer {
if (!_synthesizer) {
_synthesizer = [[AVSpeechSynthesizer alloc] init];
_synthesizer.delegate = self;
}
return _synthesizer;
}
@end
下面我們來逐一對這個 .m 文件中的每一個函數(shù)做下解釋
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {}
這個函數(shù)是通知擴展類的最為核心的函數(shù)了,你可以理解為這個就是接受到蘋果APNS 通知的一個鉤子函數(shù)赋访,每次當推送一條通知過來可都,都會執(zhí)行到這個函數(shù)體內(nèi),所以說我們的語音播報邏輯也是在這個鉤子函數(shù)中進行處理的蚓耽。
- (void)playVoiceWithContent:(NSString *)content {}
這個函數(shù)很簡單了渠牲,就是我們抽離出來的進行語音合成并播放出語音的函數(shù),我們傳遞一個語音文案作為此函數(shù)的參數(shù)即可步悠。
*- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance {}
這個函數(shù)就是我們今天的主角了签杈,我們之所以能夠?qū)崿F(xiàn)當同時有多條通知同時推送,我們還能夠一條一條的串行逐條播放鼎兽,主要的功能就歸功到這個函數(shù)了答姥,這個函數(shù)是 AVSpeechSynthesizer
類的代理函數(shù),就是一段語音播放完成后執(zhí)行這個函數(shù)谚咬,每次當一條語音播放完成鹦付,都會被此函數(shù)勾住,我們在函數(shù)體內(nèi)實現(xiàn)我們的處理邏輯择卦。
- (void)serviceExtensionTimeWillExpire {}
此函數(shù)是擴展類自帶的一個函數(shù)敲长,從這個函數(shù)解釋我們可以看出,這個函數(shù)是當擴展被系統(tǒng)終止之前秉继,會調(diào)用到這個函數(shù)祈噪。
好了,.m文件的幾個關鍵的函數(shù)我們都做了相應的解釋了尚辑,可能還有些小伙伴不是很明白辑鲤,這些和解決通知串行逐一播報有什么關系尼,下面我就來根據(jù)自己的經(jīng)驗給大家做下詳細的解釋杠茬。
先來說下蘋果通知的通知欄問題
在蘋果通知中月褥,當來一條通知時弛随,我們的手機會叮一下,然后手機通知欄彈出通知宁赤。這里大家注意下撵幽,其實這個叮一下出來的通知欄也是有生命周期的。從通知欄被彈出來礁击,到通知欄最終被收起,其實中間蘋果給了限制時間逗载,大概就6秒左右的時長
說到6秒左右的時長哆窿,對于那些多條通知同時到達,需要串行來逐一播報厉斟,但是很多小伙伴們會遇到這樣一個問題:就是當同時來了多條通知挚躯,總是只能播報2-3條,然后就語音中斷了擦秽,后面的通知不會播報了码荔,遇到這些問題的小伙伴們有沒有注意到,其實只能播報2-3條感挥,這個時間差其實就是6秒左右缩搅,也就是通知欄的生命周期時長。
出現(xiàn)上面的問題的原因就是:當?shù)谝粭l通知來了触幼,彈出通知欄硼瓣,然后開始播報第一條語音,第一條播報完了置谦,開始播報第二天語音堂鲤,可能當?shù)诙煺Z音播報到一半了,但是這個時候媒峡,通知欄周期的時間到了瘟栖,這時通知欄就會收起,注意:谅阿,當通知欄收起時半哟,擴展類里面的代碼就會終止執(zhí)行,導致后面的語音播報終端奔穿。
上面說到當通知欄收起時镜沽,擴展類的代碼會終止執(zhí)行,這里又引出了另一個注意點:就是我們創(chuàng)建的這個擴展類也是有生命周期的贱田,并且這個生命周期和通知欄的生命周期他們是有依賴關系的缅茉。即:當通知欄收起時,擴展類就會被系統(tǒng)終止男摧,擴展內(nèi)里面的代碼也會終止執(zhí)行蔬墩,只有當下一個通知欄彈出來译打,擴展類就恢復功能
上面說到通知欄的出現(xiàn)和收起能夠影響到擴展類的功能,那我們是不是控制好通知欄的顯示和隱藏拇颅,就能解決多條串行問題尼奏司?
是的,我們只要控制好通知欄樟插,就可以解決上面的棘手問題韵洋,那么問題又來了,我們怎么才能控制通知欄的顯示和隱藏尼黄锤?感覺我們平時使用蘋果的推送搪缨,從來沒有關心過處理通知欄的顯示與隱藏,感覺從來沒有這樣用過鸵熟,是的副编,對應普通的需求,我們確實不需要關系通知欄顯示隱藏流强,感覺這些蘋果系統(tǒng)自己已經(jīng)處理好了痹届,通知來了就顯示通知欄,等5秒左右打月,周期結(jié)束就隱藏通知欄队腐。
其實啊,在擴展類里面中奏篙,蘋果已經(jīng)給我們指出了如何控制通知欄的顯示和隱藏香到,核心就是這行代碼:self.contentHandler(self.bestAttemptContent);
,當我們調(diào)用到這行代碼报破,就是用來彈出通知欄的悠就,通知欄的隱藏不需要我們來控制了,因為5秒左右的生命周期結(jié)束后充易,它會自動隱藏梗脾。
是不是對這樣代碼既熟悉有陌生啊,熟悉是因為你的擴展類文件中確實有這行代碼盹靴,陌生是因為你之前從來都沒有用過這行代碼炸茧,不知道行代碼是用來干啥的。
好了稿静,既然self.contentHandler(self.bestAttemptContent);
這行核心代碼引用出來了梭冠,我們就回到最開始的問題,在沒有做任何處理時改备,為什么當同時來多條通知是控漠,語音播報就不能逐一播報尼,其實就是因為當每一條通知到達都會執(zhí)行這個函數(shù)- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {}
,有沒有發(fā)現(xiàn)盐捷,這個函數(shù)體里面 默認就是 執(zhí)行了 self.contentHandler(self.bestAttemptContent);
這行代碼偶翅。
假設 一次性同時來了 10條 通知,就會一次性調(diào)用了 10次 didReceiveNotificationRequest
這個函數(shù)碉渡, 也就 執(zhí)行了 10次 self.contentHandler(self.bestAttemptContent)
聚谁, 按照上面的說法,同時執(zhí)行10次滞诺,不就是同時彈出10次的 通知欄嗎形导,這里我調(diào)試時發(fā)現(xiàn),當同時來10條通知時习霹,通知欄并沒有同時彈出來10次骤宣,可能只彈出來1-2次。也就只能在這1-2次的時間長度中進行語音播報了序愚。
上面解釋這么多,那么我們到底該如何做尼等限,細心的同學發(fā)現(xiàn)了爸吮,我們上面 貼出來的 .m 代碼中,我們新增了一個 AVSpeechSynthesizer
類的代理函數(shù)望门,就是語音播報完成的函數(shù)形娇,我們將 呼出通知欄的代碼 self.contentHandler(self.bestAttemptContent);
添加到這個代理函數(shù)中。意思就是:當?shù)谝粭l語音播放完成了筹误,這時我們呼出通知欄顯示播放的內(nèi)容(通知欄的周期時間大概6秒左右)桐早,正好這時可以播放第二條語音,等第二條語音播放完成了厨剪,呼出第二個通知的通知欄哄酝,繼續(xù)播放第三天語音,以此類推祷膳。
看到這里陶衅,想必大家應該都理解了為啥之前總是語音播報中斷的問題。
還有一個很重要的函數(shù):- (void)serviceExtensionTimeWillExpire{}
直晨,我們上面只是提了下搀军,具體他具體有什么功能尼?
我們發(fā)現(xiàn)serviceExtensionTimeWillExpire
函數(shù)中,也調(diào)用了 self.contentHandler(self.bestAttemptContent)
這行代碼勇皇,它為啥也要調(diào)用這行代碼尼罩句?
這是因為:當我們在接受通知的鉤子函數(shù)中(didReceiveNotificationRequest
)沒有調(diào)用self.contentHandler(self.bestAttemptContent)
這行代碼,這時就會出現(xiàn)一個現(xiàn)象:就是通知收到了敛摘,但是沒有通知欄出現(xiàn)门烂,這時蘋果就不允許了。蘋果規(guī)定兄淫,當一條通知達到后诅福,如果在30秒內(nèi)匾委,還沒有呼出通知欄,我就系統(tǒng)強制調(diào)用self.contentHandler(self.bestAttemptContent)
來呼出通知欄氓润。 這時想必大家都知道 serviceExtensionTimeWillExpire
函數(shù)的用途了吧
設置支持后臺播放
- 配置應用支持后臺播放赂乐,這個只需要在Xcode中做下配置即可
這里需要注意:當勾上上面的配置后,可能會導致蘋果審核不通過咖气,這里我們可以在應用中添加一個語音播放的功能挨措,并錄制視頻告知蘋果用途,可能會過審崩溪。
iOS 10以下實現(xiàn)串行播報
核心代碼如下
// 監(jiān)聽通知函數(shù)中調(diào)用添加數(shù)據(jù)到隊列
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler {
[self addOperation: @"語音文案"];
}
#pragma mark -隊列管理推送通知
- (void)addOperation:(NSString *)title {
[[self mainQueue] addOperation:[self customOperation:title]];
}
- (NSOperationQueue *)mainQueue {
return [NSOperationQueue mainQueue];
}
- (NSOperation *)customOperation:(NSString *)content {
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
AVSpeechUtterance *utterance = nil;
@autoreleasepool {
utterance = [AVSpeechUtterance speechUtteranceWithString:content];
utterance.rate = 0.5;
}
utterance.voice = self.voiceConfig;
[self.synthConfig speakUtterance:utterance];
}];
return operation;
}
- (AVSpeechSynthesisVoice *)voiceConfig {
if (_voiceConfig == nil) {
_voiceConfig = [AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"];
}
return _voiceConfig;
}
- (AVSpeechSynthesizer *)synthConfig {
if (_synthConfig == nil) {
_synthConfig = [[AVSpeechSynthesizer alloc] init];
}
return _synthConfig;
}
注意事項
- 上面的通知擴展類最低支持iOS系統(tǒng)為 10及10 以上浅役,所以所 iOS10以下的系統(tǒng),是不支持使用通知擴展的
- 通知擴展文件中是不支持斷點調(diào)試的伶唯,網(wǎng)上有說通過配置可以進行斷點觉既,可是我嘗試了 很多次,還是不能斷點乳幸,這里我的處理方式是瞪讼,通過使用 臨時的語音播報來代替斷點,在需要斷點的地方加一個語音播放粹断,如果播報出來了符欠,代表執(zhí)行了此行
- 上面我們介紹了
speechSynthesizer:didFinishSpeechUtterance
語音播放完成的代理函數(shù),可能有的小伙伴會遇到這個代理函數(shù)不執(zhí)行的情況瓶埋,這時我們需要將AVSpeechSynthesizer
類的對象設置成全局屬性即可希柿。 - iOS 10 以下的系統(tǒng),我們也想實現(xiàn)同時多條通知的串行播報該怎么實現(xiàn)尼养筒,我自己的做法是自己維護一個數(shù)組隊列曾撤,具體的實現(xiàn)參照下面代碼塊。
-
content-avilable
字段的值晕粪,需要配置為 1 - 添加支持后天播放時盾戴,可能會被蘋果拒審
- 如何實現(xiàn)擴展類和主工程之間的數(shù)據(jù)通信(這塊內(nèi)容會單獨的出一篇文章來介紹)
- 待補充
示例Demo
https://github.com/guangqiang-liu/iOS-NotificationExtensionDemo
總結(jié)
我們公司之前做的掃碼支付需求,支付成功后播報支付金額兵多,當時在開發(fā)這塊需求時尖啡,遇到了殺進程無法進行語音播報的問題,后面引入了iOS10 的通知擴展類來解決殺進程問題剩膘。在使用擴展類時衅斩,也是遇到了不少的問題和大坑,這里就逐一做了下總結(jié)怠褐,上面的講解也是填坑后的個人理解畏梆,如有錯誤之處,歡迎留言交流指出錯誤。
更多文章
- 作者React Native開源項目OneM地址(按照企業(yè)開發(fā)標準搭建框架完成開發(fā)的):https://github.com/guangqiang-liu/OneM:歡迎小伙伴們 star
- 作者簡書主頁:包含60多篇RN開發(fā)相關的技術(shù)文章http://www.reibang.com/u/023338566ca5 歡迎小伙伴們:多多關注奠涌,多多點贊