iOS10 語音播報填坑詳解(解決串列播報中斷問題)

来源:https://www.cnblogs.com/guangqiang/archive/2018/09/18/9665796.html
-Advertisement-
Play Games

iOS10 語音播報填坑詳解(解決串列播報中斷問題) 在來聊這類需求的解決方案之前,咱們還是先來聊一聊這類需求的真實使用場景:語音播報。語音播報需求運用最為廣泛的應該是收銀對賬了,就類似於支付寶、微信、收錢吧等的收款語音提示一樣。在iOS 10 之前,蘋果沒有提供通知擴展類的時候,如果想要實現殺進程 ...


iOS10 語音播報填坑詳解(解決串列播報中斷問題)

在來聊這類需求的解決方案之前,咱們還是先來聊一聊這類需求的真實使用場景:語音播報。語音播報需求運用最為廣泛的應該是收銀對賬了,就類似於支付寶、微信、收錢吧等的收款語音提示一樣。在iOS 10 之前,蘋果沒有提供通知擴展類的時候,如果想要實現殺進程也可以正常播報語音消息很難,從ios 10添加了這一個通知擴展類後,實現殺進程播報語音就相對簡單很多了。

我們先來看一個陌生的Tagget

  • Notification Service Extension

這個Notification Service Extension 就是蘋果在 iOS 10的新系統中為我們添加的新特性,這個新特性就能幫助我們用來解決殺死進程正常語音播報

原理流程圖
原理流程圖

蘋果官方解釋:UNNotificationServiceExtension

詳細步驟

  • 創建一個通知擴展類
  • 添加語音播報邏輯代碼
  • 設置支持後臺播放
  • iOS10 一下實現串列播報

創建一個通知擴展類

  • 首先我點擊 Xcode 的 File -> New -> Target -> Notification Service Extension,新建一個通知擴展類Target。
image
image image
image

新建完後,我們的工程會多出一個文件夾,這裡示例Demo的Target命名為 NotificationSE,文件夾中有NotificationService.h NotificationService.m 文件,這兩個文件就是後面我們要用到的通知擴展類文件

image
image

在沒有對NotificationService做任何修改時,我們先來預覽下 .m 文件中都有哪些內容

image
image

從上面的截圖,我們可以看到,.m 文件其實很簡單,就 2 個函數,其實後面我們對這個文件做邏輯處理,也是很簡單的。

添加語音播報邏輯代碼

  • 註意,這裡我們使用的語音合成和播報組件也是蘋果官方提供的組件,AVSpeechSynthesizerAVSpeechSynthesisVoiceAVSpeechUtterance

我們先來看下一段語音播放代碼片段:

    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];

現在我們將 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 內容就是通知信息攜帶的數據,後面我們取語音播報的文案,通知欄的title,以及通知內容都是從這個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];
}

// 新增語音播放代理函數,在語音播報完成的代理函數中,我們添加下麵的一行代碼
- (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 文件中的每一個函數做下解釋

  • - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {}

這個函數是通知擴展類的最為核心的函數了,你可以理解為這個就是接受到蘋果APNS 通知的一個鉤子函數,每次當推送一條通知過來,都會執行到這個函數體內,所以說我們的語音播報邏輯也是在這個鉤子函數中進行處理的。

  • - (void)playVoiceWithContent:(NSString *)content {}

這個函數很簡單了,就是我們抽離出來的進行語音合成並播放出語音的函數,我們傳遞一個語音文案作為此函數的參數即可。

*- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance {}

這個函數就是我們今天的主角了,我們之所以能夠實現當同時有多條通知同時推送,我們還能夠一條一條的串列逐條播放,主要的功能就歸功到這個函數了,這個函數是 AVSpeechSynthesizer 類的代理函數,就是一段語音播放完成後執行這個函數,每次當一條語音播放完成,都會被此函數勾住,我們在函數體內實現我們的處理邏輯。

  • - (void)serviceExtensionTimeWillExpire {}

此函數是擴展類自帶的一個函數,從這個函數解釋我們可以看出,這個函數是當擴展被系統終止之前,會調用到這個函數。

好了,.m文件的幾個關鍵的函數我們都做了相應的解釋了,可能還有些小伙伴不是很明白,這些和解決通知串列逐一播報有什麼關係尼,下麵我就來根據自己的經驗給大家做下詳細的解釋。

先來說下蘋果通知的通知欄問題

在蘋果通知中,當來一條通知時,我們的手機會叮一下,然後手機通知欄彈出通知。這裡大家註意下,其實這個叮一下出來的通知欄也是有生命周期的。從通知欄被彈出來,到通知欄最終被收起,其實中間蘋果給了限制時間,大概就6秒左右的時長

說到6秒左右的時長,對於那些多條通知同時到達,需要串列來逐一播報,但是很多小伙伴們會遇到這樣一個問題:就是當同時來了多條通知,總是只能播報2-3條,然後就語音中斷了,後面的通知不會播報了,遇到這些問題的小伙伴們有沒有註意到,其實只能播報2-3條,這個時間差其實就是6秒左右,也就是通知欄的生命周期時長。

出現上面的問題的原因就是:當第一條通知來了,彈出通知欄,然後開始播報第一條語音,第一條播報完了,開始播報第二天語音,可能當第二天語音播報到一半了,但是這個時候,通知欄周期的時間到了,這時通知欄就會收起,註意:,當通知欄收起時,擴展類裡面的代碼就會終止執行,導致後面的語音播報終端。

上面說到當通知欄收起時,擴展類的代碼會終止執行,這裡又引出了另一個註意點:就是我們創建的這個擴展類也是有生命周期的,並且這個生命周期和通知欄的生命周期他們是有依賴關係的。即:當通知欄收起時,擴展類就會被系統終止,擴展內裡面的代碼也會終止執行,只有當下一個通知欄彈出來,擴展類就恢復功能

上面說到通知欄的出現和收起能夠影響到擴展類的功能,那我們是不是控制好通知欄的顯示和隱藏,就能解決多條串列問題尼?

是的,我們只要控制好通知欄,就可以解決上面的棘手問題,那麼問題又來了,我們怎麼才能控制通知欄的顯示和隱藏尼?感覺我們平時使用蘋果的推送,從來沒有關心過處理通知欄的顯示與隱藏,感覺從來沒有這樣用過,是的,對應普通的需求,我們確實不需要關係通知欄顯示隱藏,感覺這些蘋果系統自己已經處理好了,通知來了就顯示通知欄,等5秒左右,周期結束就隱藏通知欄。

其實啊,在擴展類裡面中,蘋果已經給我們指出瞭如何控制通知欄的顯示和隱藏,核心就是這行代碼:self.contentHandler(self.bestAttemptContent);,當我們調用到這行代碼,就是用來彈出通知欄的,通知欄的隱藏不需要我們來控制了,因為5秒左右的生命周期結束後,它會自動隱藏。

是不是對這樣代碼既熟悉有陌生啊,熟悉是因為你的擴展類文件中確實有這行代碼,陌生是因為你之前從來都沒有用過這行代碼,不知道行代碼是用來幹啥的。

好了,既然self.contentHandler(self.bestAttemptContent); 這行核心代碼引用出來了,我們就回到最開始的問題,在沒有做任何處理時,為什麼當同時來多條通知是,語音播報就不能逐一播報尼,其實就是因為當每一條通知到達都會執行這個函數- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {},有沒有發現,這個函數體裡面 預設就是 執行了 self.contentHandler(self.bestAttemptContent); 這行代碼。

假設 一次性同時來了 10條 通知,就會一次性調用了 10次 didReceiveNotificationRequest這個函數, 也就 執行了 10次 self.contentHandler(self.bestAttemptContent), 按照上面的說法,同時執行10次,不就是同時彈出10次的 通知欄嗎,這裡我調試時發現,當同時來10條通知時,通知欄並沒有同時彈出來10次,可能只彈出來1-2次。也就只能在這1-2次的時間長度中進行語音播報了。

上面解釋這麼多,那麼我們到底該如何做尼,細心的同學發現了,我們上面 貼出來的 .m 代碼中,我們新增了一個 AVSpeechSynthesizer 類的代理函數,就是語音播報完成的函數,我們將 呼出通知欄的代碼 self.contentHandler(self.bestAttemptContent); 添加到這個代理函數中。意思就是:當第一條語音播放完成了,這時我們呼出通知欄顯示播放的內容(通知欄的周期時間大概6秒左右),正好這時可以播放第二條語音,等第二條語音播放完成了,呼出第二個通知的通知欄,繼續播放第三天語音,以此類推。

看到這裡,想必大家應該都理解了為啥之前總是語音播報中斷的問題。

還有一個很重要的函數:- (void)serviceExtensionTimeWillExpire{},我們上面只是提了下,具體他具體有什麼功能尼?

我們發現serviceExtensionTimeWillExpire函數中,也調用了 self.contentHandler(self.bestAttemptContent) 這行代碼,它為啥也要調用這行代碼尼?

這是因為:當我們在接受通知的鉤子函數中(didReceiveNotificationRequest)沒有調用self.contentHandler(self.bestAttemptContent) 這行代碼,這時就會出現一個現象:就是通知收到了,但是沒有通知欄出現,這時蘋果就不允許了。蘋果規定,當一條通知達到後,如果在30秒內,還沒有呼出通知欄,我就系統強制調用self.contentHandler(self.bestAttemptContent) 來呼出通知欄。 這時想必大家都知道 serviceExtensionTimeWillExpire 函數的用途了吧

設置支持後臺播放

  • 配置應用支持後臺播放,這個只需要在Xcode中做下配置即可
image
image

這裡需要註意:當勾上上面的配置後,可能會導致蘋果審核不通過,這裡我們可以在應用中添加一個語音播放的功能,並錄製視頻告知蘋果用途,可能會過審。

iOS 10以下實現串列播報

核心代碼如下


// 監聽通知函數中調用添加數據到隊列
- (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系統為 10及10 以上,所以所 iOS10以下的系統,是不支持使用通知擴展的
  • 通知擴展文件中是不支持斷點調試的,網上有說通過配置可以進行斷點,可是我嘗試了 很多次,還是不能斷點,這裡我的處理方式是,通過使用 臨時的語音播報來代替斷點,在需要斷點的地方加一個語音播放,如果播報出來了,代表執行了此行
  • 上面我們介紹了speechSynthesizer:didFinishSpeechUtterance 語音播放完成的代理函數,可能有的小伙伴會遇到這個代理函數不執行的情況,這時我們需要將 AVSpeechSynthesizer 類的對象設置成全局屬性即可。
  • iOS 10 以下的系統,我們也想實現同時多條通知的串列播報該怎麼實現尼,我自己的做法是自己維護一個數組隊列,具體的實現參照下麵代碼塊。
  • content-avilable 欄位的值,需要配置為 1
  • 添加支持後天播放時,可能會被蘋果拒審
  • 如何實現擴展類和主工程之間的數據通信(這塊內容會單獨的出一篇文章來介紹)
  • 待補充

示例Demo

https://github.com/guangqiang-liu/iOS-NotificationExtensionDemo

總結

我們公司之前做的掃碼支付需求,支付成功後播報支付金額,當時在開發這塊需求時,遇到了殺進程無法進行語音播報的問題,後面引入了iOS10 的通知擴展類來解決殺進程問題。在使用擴展類時,也是遇到了不少的問題和大坑,這裡就逐一做了下總結,上面的講解也是填坑後的個人理解,如有錯誤之處,歡迎留言交流指出錯誤。

更多文章

  • 作者React Native開源項目OneM地址(按照企業開發標準搭建框架完成開發的):https://github.com/guangqiang-liu/OneM:歡迎小伙伴們 star
  • 作者簡書主頁:包含60多篇RN開發相關的技術文章http://www.jianshu.com/u/023338566ca5 歡迎小伙伴們:多多關註,多多點贊
  • 作者React Native QQ技術交流群:620792950 歡迎小伙伴進群交流學習
  • 友情提示:在開發中有遇到RN相關的技術問題,歡迎小伙伴加入交流群(620792950),在群里提問、互相交流學習。交流群也定期更新最新的RN學習資料給大家,謝謝大家支持!

歡迎小伙伴們掃下方二維碼加入RN技術交流QQ群

QQ群二維碼,500+ RN工程師在等你加入哦
QQ群二維碼,500+ RN工程師在等你加入哦

歡迎小伙伴們掃下方二維碼加入iOS技術交流QQ群

QQ群二維碼,歡迎入群
QQ群二維碼,歡迎入群
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  •     awk既然是一門解釋型語言,則就可以支持如分支語句、迴圈語句等。今天就來學習一下在awk中的分支和迴圈語句。如果您有過任何一門編程語言的基礎,則下麵所講內容也是很好理解的。 分支語句 if else awk分支語句的基本用法如下所示: 或 或 if else用法示例 三元 ...
  • 一 .概述 預設情況下, bash shell會用一些特定的環境變數來定義系統的環境。這些預設環境變數可以理解是上篇所講的系統全局環境變數。 1.1 bash shell支持的Bourne變數 Bourne shell 是 UNIX 最初使用的 shell。下麵例舉幾個常用的變數名。 變數名 描述 ...
  • 彙編語言程式搭建masm+debug 下載鏈接 dosbox:鏈接:https://pan.baidu.com/s/1TgkfU-d5w6Nz9TOYro1pYw 密碼:mp83 masm:鏈接:https://pan.baidu.com/s/1-tYpJZaoQlLpd3VHxIkMOw 密碼:8 ...
  • SPI由於介面相對簡單(只需要4根線),用途算是比較廣泛,主要應用在 EEPROM,FLASH,實時時鐘,AD轉換器,還有數字信號處理器和數字信號解碼器之間。即一個SPI的Master通過SPI與一個從設備,即上述的那些Flash,ADC等,進行通訊。而主從設備之間通過SPI進行通訊,首先要保證兩者 ...
  • 為什麼要掃描操作系統呢? 其實和上一篇博客:《服務掃描》類似,都是為了能夠發現漏洞 發現什麼漏洞? 不同的操作系統、相同操作系統不同版本,都存在著一些可以利用的漏洞 而且,不同的系統會預設開放不同的一些埠和服務 如果能夠知道操作系統和版本號,那麼就可以利用這些預設選項做一些“事情” OS的識別技術 ...
  • Linux進程的退出 linux下進程退出的方式 正常退出 從main函數返回return 調用exit 調用_exit 異常退出 調用abort 由信號終止 _exit, exit和_Exit的區別和聯繫 _exit是linux系統調用,關閉所有文件描述符,然後退出進程。 exit是c語言的庫函數 ...
  • Linux常用的命令集合 【更新中】 大家如果有不懂的,可以留言討論 常用的功能鍵: Tab 按鍵 命令補齊功能 Ctrl+c 按鍵 停掉正在運行的程式 Ctrl+d 按鍵 相當於exit,退出 Ctrl+l 按鍵 清屏 1、關機命令shutdown、halt、poweroff: 2、重啟命令reb ...
  • 【轉發】原文地址 https://www.cnblogs.com/dragonsuc/p/5512797.html mpstat -P ALL 和 sar -P ALL 說明:sar -P ALL > aaa.txt 重定向輸出內容到文件 aaa.txt 經常用來監控linux的系統狀況,比如cpu ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...