ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

iOS15后台被杀仍能语音播报:本地音频拼接与Notification Service Extension方案

iOS15后台被杀仍能语音播报:本地音频拼接与Notification Service Extension方案 简介本资源面向 iOS 开发中需要实现消息推送语音播报的工程师重点解决 iOS15 之后应用处于后台或被杀死时仍能完成语音播报的难题。方案采用本地离线音频拼接配合 Notification Service Extension规避了在线合成成本高昂的问题并针对 iOS15 本地通知通知栏重复展示、数字转语音的 numFormatter 兼容处理等细节做了修订。压缩包共 88 个文件约 19.93MB包含 18 个 mp3 音频素材、14 个 h 头文件与 11 个 m 实现文件、8 个 plist 配置、6 个 xcconfig 及 storyboard、entitlements、Podfile 等工程文件构成一套可直接编译运行的完整 Xcode 工程。已有 526 人学习下载。读者可从中获取离线音频拼接思路、Service Extension 配置方式、通知去重与数字格式化处理等可复用代码适合需要落地推送语音播报功能的中高级 iOS 开发者参考。1. 后台被杀还能播报先看清这套 iOS15 推送语音方案的底牌App 被用户从后台划掉、进程都没了推送到达时还要把金额念出来——这个需求在 iOS15 之后变得格外棘手。我手上这份 KNVoiceBroadcast4iOS15-2.0 就是专门啃这块硬骨头的工程核心思路是「本地离线合成音频 Notification Service Extension」不依赖服务端预生成语音文件也不走在线 TTS 接口。它解决三件事离线合成成本高的问题用本地拼接音频绕过去iOS15 之后本地通知在通知栏重复弹出的问题金额数字转中文时的兼容细节比如 numFormatter 在不同区域设置下的表现差异。适合正在做收款播报、订单提醒、到账语音这类场景的 iOS 开发尤其是被「后台/杀死状态仍要播报」卡住的人。工程里已经拆好了主 App、Service Extension、JPush 集成、AudioTool 工具类这几块拿到手能直接对着改。2. 拆开工程看结构主 App、Extension 与 JPush 各自管什么2.1 目录里每个文件夹对应的职责先把压缩包解开顶层是KNVoiceBroadcast.xcodeproj和KNVoiceBroadcast.xcworkspace两个工程入口日常用 workspace 打开因为里面挂了 Pods。往下看几个关键目录路径作用KNVoiceBroadcast/主 App 源码含 AppDelegate、SceneDelegate、ViewController、main.mKNVoiceBroadcast/AudioTool音频拼接与播放的核心工具类KNVoiceBroadcast/Utils金额转文字等辅助逻辑KNNotificationServiceExtension4Voice/通知服务扩展负责在推送到达时合成并播放语音Pods/JCore、Pods/JPush极光推送 SDK 及其依赖KNVoiceBroadcast.entitlements主 App 的权限配置主 App 负责注册推送、拿到 deviceToken、处理前台逻辑Extension 是独立进程系统在推送到达且 App 不在前台时唤起它给它最多 30 秒左右的时间做处理。语音播报真正发生的地方就在 Extension 里这也是「被杀死了还能播」的关键——不是主 App 在播是系统把 Extension 拉起来播。2.2 为什么必须用 Service Extension 而不是普通本地通知很多人第一反应是在didReceiveRemoteNotification里播音频但 App 被杀死时这个回调根本不会执行。iOS 的机制是带mutable-content: 1的远程推送会先交给 Notification Service Extension 处理Extension 可以修改通知内容、附件也能在这段时间里做音频播放。所以播报逻辑必须搬进 Extension。配置上要在推送 payload 里带上{ aps: { alert: { title: 收款到账, body: 微信收款 100 元 }, mutable-content: 1, sound: default }, amount: 100 }mutable-content为 1 是唤起 Extension 的开关缺了它 Extension 的didReceive不会被调用。amount是自定义字段Extension 拿它去拼接对应音频。注意 payload 总大小别超 4KB金额字段用短字符串就行。2.3 Extension 的 target 配置要点在 Xcode 里选中KNNotificationServiceExtension4Voicetarget几个地方不能漏Bundle Identifier 必须是主 App 的 ID 加后缀比如com.xxx.KNVoiceBroadcast.voiceDeployment Target 至少 iOS 15因为方案针对 iOS15 的通知行为做了适配Info.plist 里NSExtension字典的NSExtensionPointIdentifier填com.apple.usernotifications.service主 App 和 Extension 都要在 Capabilities 里打开 App Groups如果音频文件要共享Group ID 两边保持一致。Extension 的NotificationService.m里重写两个方法didReceiveNotificationRequest:withContentHandler:做合成与播放serviceExtensionTimeWillExpire做超时兜底。超时兜底一定要写否则系统强杀 Extension 时通知可能直接不显示。3. 本地拼接音频怎么落地AudioTool 与金额转文字3.1 为什么用拼接而不是实时 TTSAVSpeechSynthesizer在 Extension 里能出声但有两个坑一是首次初始化有延迟几十到几百毫秒不等推送场景下用户等不起二是 Extension 的内存和 CPU 配额有限实时合成偶尔会被系统掐掉。所以这套方案改成「预生成数字、单位、常用词的音频片段运行时按金额顺序拼接」。成本从「每次合成」降到「一次拼接」稳定性和速度都上来了。音频素材放在Assets.xcassets或独立 bundle 里命名建议用语义化前缀比如num_0到num_9、unit_shi、unit_bai、unit_wan、unit_yuan。拼接时按金额的位数依次取片段。3.2 金额转中文的 numFormatter 兼容处理金额转文字看着简单实际全是边界。NumberFormatter的numberStyle设成.spellOut能直接出中文但不同系统版本、不同 locale 下结果不一致比如「一百」和「一零零」的差异。稳妥做法是自己写映射只处理整数和小数两位// Utils/AmountToSpeech.m - (NSString *)speechTextForAmount:(NSString *)amount { // 先做基本校验避免空串或非数字导致后续越界 if (amount.length 0) return ; NSDecimalNumber *num [NSDecimalNumber decimalNumberWithString:amount]; if ([num isEqualToNumber:[NSDecimalNumber notANumber]]) return ; NSArray *digits [零,一,二,三,四,五,六,七,八,九]; NSArray *units [,十,百,千,万,十,百,千,亿]; long long value [num longLongValue]; if (value 0) return 零元; NSMutableString *result [NSMutableString string]; NSString *valueStr [NSString stringWithFormat:%lld, value]; NSInteger len valueStr.length; for (NSInteger i 0; i len; i) { int d [valueStr characterAtIndex:i] - 0; NSInteger unitIndex len - i - 1; if (d 0) { // 连续的零只保留一个且末尾零不读 if (result.length 0 ![result hasSuffix:零] unitIndex ! 0) { [result appendString:零]; } } else { [result appendFormat:%%, digits[d], units[unitIndex]]; } } [result appendString:元]; return result; }逻辑说明先校验输入decimalNumberWithString对非法串返回 NaN提前拦掉。然后逐位取数字和对应单位unitIndex是当前位在整体中的权重。零的处理是重点——连续零只读一个末尾零不读否则「100」会念成「一百零零」。参数上units数组覆盖到「亿」超过这个量级的金额业务上基本不会出现真遇到可以再扩。3.3 拼接与播放的实现拿到中文文本后按字符映射到音频文件名依次播放// AudioTool/AudioPlayer.m - (void)playSpeechForText:(NSString *)text { // 把中文文本拆成单字逐个找对应音频 NSMutableArray *files [NSMutableArray array]; for (NSInteger i 0; i text.length; i) { NSString *ch [text substringWithRange:NSMakeRange(i, 1)]; NSString *file [self audioFileNameForChar:ch]; if (file) [files addObject:file]; } // 用 AVQueuePlayer 顺序播放避免多个 AVAudioPlayer 抢声道 NSMutableArray *items [NSMutableArray array]; for (NSString *name in files) { NSURL *url [[NSBundle mainBundle] URLForResource:name withExtension:mp3]; if (url) [items addObject:[AVPlayerItem playerItemWithURL:url]]; } self.queuePlayer [AVQueuePlayer queuePlayerWithItems:items]; [self.queuePlayer play]; }逻辑说明audioFileNameForChar把「一」映射到num_1、「元」映射到unit_yuan找不到对应文件就跳过避免因为缺素材整段播不出来。用AVQueuePlayer而不是多个AVAudioPlayer是因为后者并发播放会互相打断队列播放天然串行。参数上音频片段建议统一采样率和时长否则拼接处会有明显停顿。提示Extension 里播放音频需要把音频文件同时打进 Extension target 的 Copy Bundle Resources只放主 App 是播不出来的这是最常见的翻车点。4. iOS15 通知重复弹出与后台播报的避坑清单4.1 通知栏弹出多次现象iOS15 上同一条推送在通知栏出现两三次用户以为收到多笔。原因通常是主 App 和 Extension 都调用了UNUserNotificationCenter addNotificationRequest或者 Extension 里既改了content又额外发了一条本地通知。解决Extension 只负责修改bestAttemptContent并调用contentHandler不要再手动发本地通知主 App 在前台时用willPresentNotification返回展示选项别重复 add。4.2 后台/杀死状态播报无声现象App 在前台能播切后台或划掉后推送到了但没声音。原因有三类payload 缺mutable-content: 1Extension 的音频文件没打进 bundleExtension 里播放代码在contentHandler之后执行进程已被回收。解决先确认 payload再检查 Extension target 的 Copy Bundle Resources最后把播放逻辑放在contentHandler调用之前或者用dispatch_after留出播放时间再回调。4.3 金额念错或漏字现象「100」念成「一百零零」「1005」念成「一千零五」少了「零」。原因是零的处理逻辑没覆盖中间零和末尾零的差异。解决按 3.2 的规则连续零只保留一个、末尾零不读中间零必须读。测试时把 10、100、1005、1010、10000 这几个值都跑一遍基本能覆盖大部分边界。4.4 Extension 超时被系统杀掉现象偶发推送到了但通知不显示日志里能看到 Extension 被终止。原因是合成或播放耗时超过系统给的窗口。解决实现serviceExtensionTimeWillExpire在里面立刻调用contentHandler把当前bestAttemptContent交出去保证通知至少能显示音频拼接提前在didReceive开头就启动别等网络请求回来再开始。4.5 JPush 自定义字段拿不到现象didReceive里读request.content.userInfo取不到amount。原因是极光推送的自定义字段默认放在userInfo顶层但如果服务端把它塞进了aps里就会被系统过滤。解决自定义字段一律放 payload 顶层别放aps内同时确认极光控制台的推送模板没有覆盖字段。5. 进阶把播报做成可配置、可验证的稳定链路走到这一步基本链路已经通了但真上线前还有两件事值得做。第一件是把音频素材和金额规则做成可配置。业务上金额播报往往有变体比如「到账 100 元」和「收款 100 元」前缀不同硬编码在代码里改一次要重新发版。我的做法是把前缀音频也做成片段用App Groups共享一个 plist 配置Extension 启动时读一次运营侧改配置不用动代码。第二件是验证。推送播报这种链路光靠手点很容易漏掉边界。我一般会写一个本地调试入口在主 App 里模拟构造 payload 直接喂给 Extension 的处理逻辑把金额、前缀、是否静音这几个参数做成开关跑一轮覆盖表场景金额预期播报整数100一百元带零1005一千零五元中间零1010一千零一十元大额10000一万元小额1一元这张表跑通线上大部分金额都不会念错。另外 Extension 的日志默认不进主 App 的 console调试时用os_log加 subsystem 过滤或者在 Xcode 里 attach 到 Extension 进程看输出别用NSLog干等。还有个容易忽略的点iOS15 之后通知摘要Notification Summary可能把推送延后展示语音播报的时机和用户看到通知的时机对不上。如果业务对实时性要求高payload 里把interruption-level设成time-sensitive并在 entitlement 里申请对应权限能减少被摘要拦截的概率。这个不是万能药但比默认行为可控。从那以后我每次接推送播报类需求都强制先把「payload 字段 → Extension 处理 → 音频拼接 → 超时兜底」这条链路在真机上从后台、杀死、前台三种状态各跑一遍再谈业务逻辑。希望帮到你。本文还有配套的精品资源点击获取
返回列表