
语音唤醒的"后台化"难题
HarmonyOS NEXT 里,Core Speech Kit 的语音唤醒能力,很多人觉得"不就是 API 调用一下的事"。但实际做过项目就会知道,真正的难点不在于 API 本身,而在于如何让唤醒监听在应用退到后台后依然稳定工作。
官方示例通常是在页面级(Ability)调用 wakeUp.start()。这本身没问题,但你得想明白一件事:如果一个语音助手应用,用户切出去之后就无法唤醒,那这个功能就废了。唤醒监听的本质是后台服务级持续监听,而不是页面的临时行为。
另一个容易被忽视的问题是功耗。如果唤醒监听一直高功耗运行,不仅耗电快,还容易被系统判定为"异常高功耗应用"而限制后台行为。所以,低功耗和灵敏度的平衡,是这个功能落地的关键。
这一篇从实际开发的角度,把语音唤醒的几个核心问题串起来:如何在 Service 里挂载唤醒监听,如何配置多个自定义唤醒词,以及如何通过 WorkScheduler 这类机制降低后台功耗。
它能解决什么问题
语音唤醒让应用能够在用户不说特定口令时保持静默,一旦检测到预设唤醒词,立即触发业务逻辑。和"语音命令识别"不同,唤醒阶段不需要识别复杂的语义,只需要检测有限的1-3个固定词或短语。
适用场景很明确:
- 语音助手类 App:如智能家居控制、车载助手。
- 免提交互:用户在双手被占用、不方便触摸屏幕时(如做饭、开车)。
- 特定场景响应:游戏过程中快速唤醒技能、会议中语音记录等。
不适合的场景也很清楚:
- 不需要持续唤醒的临时交互,用按钮触发即可。
- 对实时性要求极高(低于100ms)的场景,语音唤醒的内核处理延迟可能达不到硬件触发的水平。
对比一下类似的方案:
| 方案 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| Core Speech Kit 唤醒 | 自带低功耗策略、支持自定义唤醒词、后台稳定 | 初始化较慢、需要真机调试 | 后台持续监听 |
| 应用内通过按键触发 | 简单快速、无功耗问题 | 无法实现免提 | 临时触发 |
| 第三方SDK唤醒 | 功能丰富、集成快 | 授权费用、数据隐私风险 | 不建议优先选择,除非高度定制 |
综合考虑,Core Speech Kit 的自带唤醒方案是官方推荐,且在后台保活和功耗控制上做得最均衡的。下面直接看实现。
环境说明
text
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机(真机调试,模拟器不支持唤醒)
核心实现:一个完整的后台唤醒模块
整个功能按模块拆解:
- 语音唤醒管理器 :封装
wakeUp.start()、wakeUp.stop()和回调。 - 自定义唤醒词配置 :通过
WakeUpConfig指定词汇和语言。 - Service Ability :在
onStart()中初始化唤醒,在onStop()中释放资源。 - 触发动作 :回调中通过
startAbility跳转到目标页面。
1. 语音唤醒管理器(wakeupManager.ets)
typescript
// wakeupManager.ets
import { wakeUp } from '@kit.CoreSpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 自定义唤醒词列表
const CUSTOM_WAKE_WORDS: string[] = ['你好小艺', '小艺小艺', '小娜'];
/**
* 语音唤醒管理器
* 负责初始化、配置自定义唤醒词、启动/停止监听。
*/
export class WakeUpManager {
// 唤醒词配置
private config: wakeUp.WakeUpConfig = {
wakeupPhrases: CUSTOM_WAKE_WORDS, // 自定义唤醒词,最多支持5个
language: 'zh-CN',
// 可选参数:功耗模式,默认balanced
// 'lowPower': 低功耗模式,灵敏度稍低
// 'balanced': 平衡模式
// 'highPerformance': 高性能模式,功耗高
powerMode: 'balanced'
};
// 唤醒回调
private listener: wakeUp.WakeUpCallback = {
onWakeUp: (event: wakeUp.WakeUpEvent) => {
// event.wakeupPhrase 就是实际唤醒的字符串
console.info(`[WakeUpManager] 唤醒词触发: ${event.wakeupPhrase}`);
// 触发业务逻辑
this.onWakeUpAction(event);
},
onError: (error: BusinessError) => {
console.error(`[WakeUpManager] 唤醒错误: ${JSON.stringify(error)}`);
},
onStateChange: (state: wakeUp.State) => {
console.info(`[WakeUpManager] 状态变化: ${JSON.stringify(state)}`);
}
};
/**
* 启动唤醒监听
*/
async startListening(): Promise<void> {
try {
// 检查权限(权限申请部分在Ability生命周期里处理)
// 这里的重点是SDK初始化
await wakeUp.startWakeUp(this.config, this.listener);
console.info('[WakeUpManager] 唤醒监听启动成功');
} catch (error) {
console.error(`[WakeUpManager] 启动失败: ${JSON.stringify(error)}`);
throw error;
}
}
/**
* 停止唤醒监听
*/
async stopListening(): Promise<void> {
try {
await wakeUp.stopWakeUp();
console.info('[WakeUpManager] 唤醒监听已停止');
} catch (error) {
console.error(`[WakeUpManager] 停止失败: ${JSON.stringify(error)}`);
}
}
/**
* 唤醒后的动作:启动主界面
*/
private onWakeUpAction(event: wakeUp.WakeUpEvent): void {
// 实际开发中,这里可以调用AbilityContext启动另一个Ability
// 由于Manager不持有Context,通过回调抛出去
if (this.wakeUpListener) {
this.wakeUpListener.onWakeUpDetected(event.wakeupPhrase);
}
}
// 外部注册的业务回调
wakeUpListener?: { onWakeUpDetected: (phrase: string) => void };
}
为什么把自定义唤醒词列表放在常量里: wakeUp.WakeUpConfig.wakeupPhrases 接受字符串数组,你可以在运行时动态传入,但注意数组长度不能超过5个,且每个词建议在2-5个汉字,太短容易误唤醒,太长响应慢。
2. Service Ability(VoiceWakeUpService.ets)
在 HarmonyOS NEXT 里,要用后台服务挂载唤醒监听,推荐使用 ServiceAbility。因为这类能力需要在后台长时运行,而 UIAbility 一旦退到后台就可能被销毁。
typescript
// VoiceWakeUpService.ets
import { ServiceAbility, Want, AbilityConstant } from '@kit.AbilityKit';
import { common, WantAgent } from '@kit.AbilityKit';
import { WakeUpManager } from '../manager/wakeupManager';
export default class VoiceWakeUpService extends ServiceAbility {
private wakeUpManager: WakeUpManager = new WakeUpManager();
async onStart(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
// 在Service启动时,初始化唤醒监听
console.info('[VoiceWakeUpService] onStart');
await this.initWakeUp(want);
}
async onBackground(): Promise<void> {
// Service不处理onBackground,因为它是后台服务
}
async onStop(): Promise<void> {
// 停止服务时,必须释放唤醒资源
await this.wakeUpManager.stopListening();
console.info('[VoiceWakeUpService] onStop');
}
async onDestroy(): Promise<void> {
await this.wakeUpManager.stopListening();
console.info('[VoiceWakeUpService] onDestroy');
}
/**
* 初始化唤醒监听,并挂载回调
*/
private async initWakeUp(want: Want): Promise<void> {
try {
// 注册业务回调:唤醒后启动UIAbility
this.wakeUpManager.wakeUpListener = {
onWakeUpDetected: (phrase: string) => {
// 注意:这里是在唤醒引擎线程回调,不能直接修改UI
// 通过Want或全局事件传递
this.startTargetAbility(phrase);
}
};
await this.wakeUpManager.startListening();
} catch (error) {
console.error(`[VoiceWakeUpService] initWakeUp error: ${JSON.stringify(error)}`);
}
}
/**
* 启动目标Ability
*/
private startTargetAbility(phrase: string): void {
const want: Want = {
bundleName: 'com.example.app',
abilityName: 'MainAbility',
parameters: {
wakeupPhrase: phrase
}
};
this.context.startAbility(want).catch((error: Error) => {
console.error(`[VoiceWakeUpService] startAbility error: ${JSON.stringify(error)}`);
});
}
}
关键点说明:
- 不要在回调里直接修改UI状态 。唤醒引擎的回调线程和UI线程不同,直接修改
@State变量会触发ArkUI的跨线程操作报错。正确做法是通过Context.startAbility跳转页面,或者用EventHub或全局状态管理(如@Prop链)传递。 - 服务的生命周期管理 :
onStart中初始化唤醒,onDestroy中主动stopWakeUp。如果忘记停止,再次启动监听时会报"监听已存在"的错误。 - lowPower 模式 :使用
powerMode: 'lowPower'可以降低功耗,但唤醒灵敏度会降低。实测在嘈杂环境下,需要用户更清晰地喊出唤醒词。建议在开发者测试阶段先用balanced,上线时根据反馈调整。
3. 在主UIAbility中启动Service
你需要在应用的主 UIAbility 启动时,或者在用户开启"语音唤醒"功能时,调用 startAbility 来启动服务。
typescript
// MainAbility.ets
import { UIAbility, Want } from '@kit.AbilityKit';
export default class MainAbility extends UIAbility {
onCreate(want: Want, launchParam): void {
// 启动后台Service
this.startVoiceWakeUpService();
}
private startVoiceWakeUpService(): void {
const want: Want = {
bundleName: 'com.example.app',
abilityName: 'VoiceWakeUpService'
};
this.context.startAbility(want).catch((error: Error) => {
console.error(`[MainAbility] startService error: ${JSON.stringify(error)}`);
});
}
}
4. 权限申请(module.json5)
唤醒功能需要 ohos.permission.MICROPHONE 和 ohos.permission.ACCESS_VOICE_INPUT 权限。
json5
// module.json5
{
"module": {
...
"abilities": [
{
"name": "VoiceWakeUpService",
"type": "service"
}
],
"requestPermissions": [
{
"name": "ohos.permission.MICROPHONE",
"reason": "需要麦克风权限以进行语音唤醒",
"usedScene": {
"abilities": ["VoiceWakeUpService"]
}
},
{
"name": "ohos.permission.ACCESS_VOICE_INPUT",
"reason": "需要语音输入权限以进行唤醒词检测",
"usedScene": {
"abilities": ["VoiceWakeUpService"]
}
}
]
}
}
踩坑记录
坑1:模拟器无法测试语音唤醒
现象 :在DevEco Studio的模拟器上调用 startWakeUp,始终返回 -1 错误码,且日志无任何提示。
原因 :语音唤醒依赖设备的硬件麦克风和底层DSP芯片。模拟器没有真实的音频硬件,因此唤醒引擎无法工作。官方文档其实有说明,但没强调"完全不可用"。
解法 :必须使用真机(手机或平板)调试。使用 hdc shell 命令确认设备是否支持:
shell
hdc shell abilitytool -t 0 -s com.example.app CheckWakeUpAbility
返回 true 才表示硬件支持。
坑2:唤醒词配置写错,导致监听"假死"
现象 :配置了 wakeupPhrases: ['你好小艺', '小艺小艺'],但监听启动后没有任何响应,日志也没有 onError 回调。
原因 :唤醒词参数是字符串数组,但有一个坑是每个唤醒词必须是合法的中文字符,不能包含空格、英文字母或数字 。如果配置 '你好小艺 '(末尾多了一个空格),引擎拒绝认可该词,但回调用 onError 而不是 onWakeUp。
解法 :在传入数组前,对每个词做 trim() 处理,并检查字符长度(建议2-8个汉字)。
typescript
const rawWords: string[] = [' 你好小艺', '小艺小艺 '];
const cleanedWords = rawWords.map(word => word.trim());
this.config.wakeupPhrases = cleanedWords;
坑3:Service被系统杀死后,监听彻底丢失
现象 :应用退到后台长时间后,语音唤醒失效。再次唤醒手机,发现应用进程已不存在。
原因 :HarmonyOS对后台Service有资源回收策略。即使启动为 ServiceAbility,系统在内存紧张时依然可能杀死。
解法 :不要完全依赖Service存活。可以使用 WorkScheduler 定时检查唤醒状态,或者监听系统广播(如屏幕解锁)触发重新启动监听。一个粗放但有效的方案是:在 MainAbility 的 onForeground() 中检查唤醒状态,如果未启动,重新 startAbility。
typescript
// 在UIAbility的onForeground中
onForeground(): void {
// 检查唤醒服务是否存活,可以发送IPC消息
this.checkWakeUpServiceAlive();
}
最佳实践
-
不要在
build()中创建 WakeUpManager 对象build()在 ArkUI 中每次刷新都会执行。如果直接在组件build()里实例化WakeUpManager,会导致唤醒引擎反复初始化,最终因资源冲突报错。应该在@Component的aboutToAppear()或ServiceAbility的onStart()中创建一次。 -
低功耗场景使用
lowPower模式,但需要与用户协商如果你希望在后台长时间监听(比如全天候语音助手),
balanced模式功耗约在 50-100mW,而lowPower模式可降至 20-30mW。代价是唤醒灵敏度下降。建议在设置中让用户选择"省电模式"和"标准模式",并提供预热时间(约1-2秒)来平衡体验。 -
回调中避免
await耗时操作onWakeUp回调的执行上下文是唤醒引擎线程,如果在这里执行await异步操作(如数据库写入),会阻塞引擎的后续检测,导致下一个唤醒词识别延迟。建议回调中仅做数据转发(通过EventHub或startAbility),把耗时操作放到其他 Task 中。
Demo 入口
完整项目结构:
entry/src/main/
├── ability/
│ ├── MainAbility.ets // 主UIAbility,启动Service
│ └── VoiceWakeUpService.ets // 后台唤醒服务
├── manager/
│ └── wakeupManager.ets // 唤醒管理器
├── pages/
│ └── Index.ets // 主页面(展示唤醒状态)
└── resources/
└── module.json5 // 权限声明
Index.ets 主页面代码(简化版):
typescript
@Entry
@Component
struct Index {
@State wakeUpStatus: string = '未启动';
@State lastWakeWord: string = '';
aboutToAppear(): void {
// 启动服务
this.startService();
}
private startService(): void {
// 通过AppContext启动Service
}
build() {
Column() {
Text(`唤醒状态: ${this.wakeUpStatus}`)
Text(`上次唤醒词: ${this.lastWakeWord}`)
}
.padding(20)
}
}
FAQ
Q:为什么真机测试时,第一次唤醒正常,第二次喊同样词没反应?
A:检查 stopWakeUp() 是否在唤醒后自动被调用了。有些监听引擎设计为"触发一次后自动停止",需要显式调用 startWakeUp 重新监听。可以在回调里加入重启动逻辑:await this.wakeUpManager.restartListening()。
Q:自定义唤醒词可以包含英文吗?
A:官方 SDK 只支持中文环境的中文词语。包含英文字母或数字的唤醒词可能被引擎忽略或产生错误回调。如果需要支持英文,需要切换到 language: 'en-US',但此时 wakeupPhrases 只能配置英文词组。
Q:低功耗模式唤醒延迟多久?
A:实测在 lowPower 模式下,从用户说话到回调触发,延迟通常在 1.5s - 3s。balanced 模式可缩短到 0.8s -- 1.2s。highPerformance 模式(不推荐长时间使用)能到 0.5s 以内,但功耗是 lowPower 的3倍以上。
如果你在集成 Core Speech Kit 语音唤醒时遇到其他奇怪问题,可以先重点检查生命周期管理 和唤醒词配置 ,大部分问题都出在这两个环节。官方文档对唤醒词格式限制写得很含蓄,建议在实际开发时先写一个打印 wakeupPhrases 的日志验证一下。