在这里插入图片描述

语音唤醒的“后台化”难题

HarmonyOS NEXT 里,Core Speech Kit 的语音唤醒能力,很多人觉得“不就是 API 调用一下的事”。但实际做过项目就会知道,真正的难点不在于 API 本身,而在于如何让唤醒监听在应用退到后台后依然稳定工作

官方示例通常是在页面级(Ability)调用 wakeUp.start()。这本身没问题,但你得想明白一件事:如果一个语音助手应用,用户切出去之后就无法唤醒,那这个功能就废了。唤醒监听的本质是后台服务级持续监听,而不是页面的临时行为。

另一个容易被忽视的问题是功耗。如果唤醒监听一直高功耗运行,不仅耗电快,还容易被系统判定为“异常高功耗应用”而限制后台行为。所以,低功耗和灵敏度的平衡,是这个功能落地的关键。

这一篇从实际开发的角度,把语音唤醒的几个核心问题串起来:如何在 Service 里挂载唤醒监听,如何配置多个自定义唤醒词,以及如何通过 WorkScheduler 这类机制降低后台功耗。

它能解决什么问题

语音唤醒让应用能够在用户不说特定口令时保持静默,一旦检测到预设唤醒词,立即触发业务逻辑。和“语音命令识别”不同,唤醒阶段不需要识别复杂的语义,只需要检测有限的1-3个固定词或短语。

适用场景很明确:

  • 语音助手类 App:如智能家居控制、车载助手。
  • 免提交互:用户在双手被占用、不方便触摸屏幕时(如做饭、开车)。
  • 特定场景响应:游戏过程中快速唤醒技能、会议中语音记录等。

不适合的场景也很清楚:

  • 不需要持续唤醒的临时交互,用按钮触发即可。
  • 对实时性要求极高(低于100ms)的场景,语音唤醒的内核处理延迟可能达不到硬件触发的水平。

对比一下类似的方案:

方案 优点 缺点 推荐场景
Core Speech Kit 唤醒 自带低功耗策略、支持自定义唤醒词、后台稳定 初始化较慢、需要真机调试 后台持续监听
应用内通过按键触发 简单快速、无功耗问题 无法实现免提 临时触发
第三方SDK唤醒 功能丰富、集成快 授权费用、数据隐私风险 不建议优先选择,除非高度定制

综合考虑,Core Speech Kit 的自带唤醒方案是官方推荐,且在后台保活和功耗控制上做得最均衡的。下面直接看实现。

环境说明

DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机(真机调试,模拟器不支持唤醒)

核心实现:一个完整的后台唤醒模块

整个功能按模块拆解:

  1. 语音唤醒管理器:封装 wakeUp.start()wakeUp.stop() 和回调。
  2. 自定义唤醒词配置:通过 WakeUpConfig 指定词汇和语言。
  3. Service Ability:在 onStart() 中初始化唤醒,在 onStop() 中释放资源。
  4. 触发动作:回调中通过 startAbility 跳转到目标页面。
1. 语音唤醒管理器(wakeupManager.ets)
// 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 一旦退到后台就可能被销毁。

// 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 来启动服务。

// 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.MICROPHONEohos.permission.ACCESS_VOICE_INPUT 权限。

// 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 命令确认设备是否支持:

hdc shell abilitytool -t 0 -s com.example.app CheckWakeUpAbility

返回 true 才表示硬件支持。

坑2:唤醒词配置写错,导致监听“假死”

现象:配置了 wakeupPhrases: ['你好小艺', '小艺小艺'],但监听启动后没有任何响应,日志也没有 onError 回调。
原因:唤醒词参数是字符串数组,但有一个坑是每个唤醒词必须是合法的中文字符,不能包含空格、英文字母或数字。如果配置 '你好小艺 '(末尾多了一个空格),引擎拒绝认可该词,但回调用 onError 而不是 onWakeUp
解法:在传入数组前,对每个词做 trim() 处理,并检查字符长度(建议2-8个汉字)。

const rawWords: string[] = [' 你好小艺', '小艺小艺 '];
const cleanedWords = rawWords.map(word => word.trim());
this.config.wakeupPhrases = cleanedWords;
坑3:Service被系统杀死后,监听彻底丢失

现象:应用退到后台长时间后,语音唤醒失效。再次唤醒手机,发现应用进程已不存在。
原因:HarmonyOS对后台Service有资源回收策略。即使启动为 ServiceAbility,系统在内存紧张时依然可能杀死。
解法:不要完全依赖Service存活。可以使用 WorkScheduler 定时检查唤醒状态,或者监听系统广播(如屏幕解锁)触发重新启动监听。一个粗放但有效的方案是:在 MainAbilityonForeground() 中检查唤醒状态,如果未启动,重新 startAbility

// 在UIAbility的onForeground中
onForeground(): void {
  // 检查唤醒服务是否存活,可以发送IPC消息
  this.checkWakeUpServiceAlive();
}

最佳实践

  1. 不要在 build() 中创建 WakeUpManager 对象
    build() 在 ArkUI 中每次刷新都会执行。如果直接在组件 build() 里实例化 WakeUpManager,会导致唤醒引擎反复初始化,最终因资源冲突报错。应该在 @ComponentaboutToAppear()ServiceAbilityonStart() 中创建一次。

  2. 低功耗场景使用 lowPower 模式,但需要与用户协商
    如果你希望在后台长时间监听(比如全天候语音助手),balanced 模式功耗约在 50-100mW,而 lowPower 模式可降至 20-30mW。代价是唤醒灵敏度下降。建议在设置中让用户选择“省电模式”和“标准模式”,并提供预热时间(约1-2秒)来平衡体验。

  3. 回调中避免 await 耗时操作
    onWakeUp 回调的执行上下文是唤醒引擎线程,如果在这里执行 await 异步操作(如数据库写入),会阻塞引擎的后续检测,导致下一个唤醒词识别延迟。建议回调中仅做数据转发(通过 EventHubstartAbility),把耗时操作放到其他 Task 中。

Demo 入口

完整项目结构:

entry/src/main/
├── ability/
│   ├── MainAbility.ets        // 主UIAbility,启动Service
│   └── VoiceWakeUpService.ets // 后台唤醒服务
├── manager/
│   └── wakeupManager.ets      // 唤醒管理器
├── pages/
│   └── Index.ets              // 主页面(展示唤醒状态)
└── resources/
    └── module.json5           // 权限声明

Index.ets 主页面代码(简化版):

@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 的日志验证一下。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐