第4.6篇:语音识别状态管理------开始、停止与异常处理
系列 :鸿蒙系统能力与设备协同篇
难度 :⭐⭐⭐ 高级
前置知识 :4.5 语音识别实战
涉及源文件 :
products/default/src/main/ets/services/VoiceRecognitionService.ets

在上一篇中,我们实现了语音识别的完整接入------创建 SpeechRecognitionEngine、配置 AudioCapturer 进行音频采集、通过 RecognitionListener 获取识别结果。然而,一个生产级的语音识别服务不仅要"能跑起来",还需要可靠地管理状态:防止重复启动、在适当的时机释放资源、优雅地结束会话、以及从各种异常中恢复。
本文将深入 VoiceRecognitionService 的内部,剖析其异步状态机设计、资源释放策略、以及异常处理机制。这些模式不仅适用于语音识别,也是鸿蒙中所有长生命周期系统能力(如相机、音频播放、网络连接)的通用最佳实践。
一、为什么需要状态管理?
语音识别涉及多个系统级资源的协作:
- SpeechRecognitionEngine:语音识别引擎,持有与系统 AI 服务的连接
- AudioCapturer:音频采集器,占用麦克风硬件
- 音频读取循环:持续从麦克风读取 PCM 数据并写入引擎
- RecognitionListener:异步回调,可能在任何时刻触发
如果这些资源的状态管理不当,会出现以下问题:
| 问题 | 后果 |
|---|---|
| 用户快速点击"开始/停止" | 多个音频采集器同时运行,麦克风冲突 |
| 页面退出时未停止识别 | 引擎和采集器资源泄漏,系统服务残留 |
| 异常发生后直接重试 | 旧资源未释放,新资源创建失败 |
多次调用 destroy |
空指针异常或重复释放 |
解决这些问题的方式,就是引入状态机------让服务在任何时刻都处于一个明确的、可预测的状态,所有操作只在合法的状态下执行。
二、四状态状态机设计
VoiceRecognitionService 内部维护了一个隐式状态机 ,通过 recording 布尔值和对象引用(engine、capturer)的 null 状态来区分四个状态:
┌──────────────────────────────────────┐
│ 状态转换图 │
└──────────────────────────────────────┘
┌─────────┐ start() ┌──────────┐ onResult ┌──────────┐
│ IDLE │────────────→│ RECORDING │───isLast=true──→│PROCESSING│
│ 空闲 │ │ 录音中 │ │ 识别中 │
└────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
│ │ stop() │ onComplete
│ │ engine.finish() │ releaseEngine
│ ▼ │
│ ┌──────────┐ │
│ │ IDLE │◄──────────────────────┘
│ │ 空闲 │
│ └──────────┘
│ ▲
│ │
└──────────────────────────┘
任何异常 → destroy()
四个状态的定义
| 状态 | recording | engine/capturer | 含义 |
|---|---|---|---|
| IDLE 空闲 | false |
null |
未开始识别,所有资源已释放,可安全调用 start() |
| RECORDING 录音中 | true |
非 null |
正在从麦克风采集音频并送入引擎,可调用 stop() 或等待结果 |
| PROCESSING 识别中 | false |
engine 非 null,capturer 已释放 |
麦克风已关闭,引擎正在处理最终结果,等待 onComplete 回调 |
| ERROR 错误 | false |
已释放或释放中 | 发生异常,调用 destroy() 回到 IDLE |
这四种状态覆盖了语音识别的完整生命周期。特别值得注意的是 PROCESSING 状态------它在 onResult 返回 isLast = true 时进入,此时录音已停止但引擎仍在处理最终结果,等待 onComplete 回调后才会释放引擎资源。
三、start():IDLE → RECORDING 的状态跃迁
start() 方法是整个服务的入口,负责完成从 IDLE 到 RECORDING 的状态跃迁。
3.1 状态守卫
typescript
async start(callbacks: VoiceRecognitionCallbacks): Promise<void> {
if (this.recording) {
return; // 状态守卫:已在录音中则直接返回
}
this.recording = true; // → RECORDING
this.sessionId = Date.now().toString();
callbacks.onListeningChange(true);
callbacks.onStatus('正在准备语音识别');
// ...
}
if (this.recording) return; 是一个状态守卫(State Guard)。当用户因网络延迟或界面响应滞后而快速点击两次"开始"时,第二次调用会被直接忽略,防止重复启动。
这与第 4.5 篇中 canIUse 的能力检测思路一脉相承------在操作前先检查前置条件,不符合则提前返回,避免资源浪费和异常。
3.2 异步初始化顺序
进入 RECORDING 状态后,start() 按严格顺序执行一系列异步初始化操作:
typescript
try {
// 1. 能力检测
if (!canIUse(SPEECH_RECOGNIZER_CAPABILITY)) {
throw new Error('当前设备不支持系统语音识别能力');
}
// 2. 创建引擎
this.engine = await this.createSpeechEngine(callbacks);
// 3. 注册回调监听
this.engine.setListener(this.createListener(callbacks));
// 4. 创建音频采集器
this.capturer = await audio.createAudioCapturer(this.createCapturerOptions());
// 5. 开始监听
this.engine.startListening({ sessionId: this.sessionId, /* ... */ });
// 6. 启动采集
await this.capturer.start();
// 7. 启动音频读取循环(不阻塞)
this.readAudioLoop(callbacks);
} catch (error) {
// 任何一步失败 → 统一错误处理
callbacks.onError(this.formatError(error as BusinessError, '语音识别启动失败,请稍后重试'));
callbacks.onListeningChange(false);
await this.destroy(); // → IDLE
}
这七步操作中任何一步抛出异常,都会进入 catch 块:先通过回调通知调用方,然后调用 destroy() 将所有资源恢复到 IDLE 状态。这种统一异常处理模式确保不会出现"引擎创建成功但采集器启动失败"这种半初始化状态。
3.3 引擎创建的回退策略
createSpeechEngine 方法内部实现了在线/离线双模式回退策略:
typescript
private async createSpeechEngine(callbacks: VoiceRecognitionCallbacks): Promise<speechRecognizer.SpeechRecognitionEngine> {
const modes: SpeechEngineMode[] = [
{ mode: SPEECH_MODE_OFFLINE, label: '离线' },
{ mode: SPEECH_MODE_ONLINE, label: '在线' }
];
let lastErrorMessage: string = '';
for (let i = 0; i < modes.length; i++) {
callbacks.onStatus('正在启动' + modes[i].label + '语音识别');
try {
return await speechRecognizer.createEngine({
language: 'zh-CN',
online: modes[i].mode
});
} catch (error) {
lastErrorMessage = modes[i].label + '模式' + this.formatError(error as BusinessError, '创建失败');
}
}
throw new Error('语音识别引擎创建失败,' + lastErrorMessage);
}
代码会先尝试离线模式(mode = 1),如果失败再尝试在线模式(mode = 0)。两种模式都失败时,才抛出最终错误。这种降级策略在多种设备上都能获得最佳兼容性------某些设备可能只支持离线识别,而某些场景下在线识别因网络问题不可用。
四、stop():RECORDING → IDLE 的优雅终止
stop() 方法负责将状态从 RECORDING 转换回 IDLE,其核心设计理念是优雅终止------尽可能让引擎完成当前音频片段的识别,而不是粗暴中断。
typescript
async stop(): Promise<void> {
if (!this.recording) {
return; // 状态守卫:不在录音中则直接返回
}
this.recording = false; // → IDLE(标记状态)
await this.releaseCapturer(); // Step 1: 停止采集
if (this.engine !== null && this.sessionId !== '') {
try {
this.engine.finish(this.sessionId); // Step 2: 结束会话
} catch {
this.releaseEngine(); // finish 失败 → 强制释放
}
}
}
4.1 两步释放策略
stop() 的资源释放分为两个步骤:
Step 1 --- 释放采集器 :releaseCapturer() 停止并释放 AudioCapturer,关闭麦克风。此时引擎仍在工作,处理已采集的最后一段音频。
Step 2 --- 结束引擎会话 :调用 engine.finish(sessionId) 通知引擎当前会话结束。finish 是一个优雅关闭 操作------引擎会完成对已接收音频的识别处理,然后触发 onComplete 回调。
这种"先停采集、再结束会话"的顺序至关重要:如果先结束会话再释放采集器,引擎可能在处理过程中仍在等待更多音频数据;如果仅释放采集器而不调用 finish,引擎的会话会一直挂起,导致资源无法回收。
4.2 finish 异常的兜底
如果 engine.finish() 抛出异常(例如引擎已被其他操作销毁),catch 块会直接调用 releaseEngine() 强制释放引擎资源。这是一种安全的降级------虽然失去了优雅结束的机会,但至少不会造成资源泄漏。
五、destroy():强制重置到 IDLE
destroy() 是最终的"大扫除"方法,无论服务处于什么状态,调用后都会回到干净的 IDLE 状态:
typescript
async destroy(): Promise<void> {
this.recording = false; // → IDLE
await this.releaseCapturer();
if (this.engine !== null && this.sessionId !== '') {
try {
this.engine.cancel(this.sessionId); // 强制取消会话
} catch {
}
}
this.releaseEngine();
this.sessionId = '';
}
与 stop() 不同,destroy() 使用 engine.cancel(sessionId) 而非 finish()。这两者的区别至关重要:
| 方法 | 行为 | 适用场景 |
|---|---|---|
engine.finish(sessionId) |
完成当前音频处理,触发 onComplete 回调 |
正常停止,保留识别结果 |
engine.cancel(sessionId) |
立即终止会话,不触发任何回调 | 异常恢复、页面销毁 |
cancel() 是强制取消 ,不会等待引擎完成处理,也不会触发 onComplete 或 onResult。这在异常处理场景中非常合适------当出现错误时,我们不需要已损坏的识别结果,只需快速释放资源。
六、资源释放:releaseCapturer + releaseEngine
资源释放是状态管理的核心保障。VoiceRecognitionService 将资源释放抽象为两个独立的方法,各自负责一类资源的完整生命周期。
6.1 releaseCapturer:音频采集器释放
typescript
private async releaseCapturer(): Promise<void> {
const currentCapturer: audio.AudioCapturer | null = this.capturer;
this.capturer = null; // 先置空引用
if (currentCapturer === null) {
return;
}
try {
await currentCapturer.stop(); // 停止采集
} catch {
}
try {
await currentCapturer.release(); // 释放硬件资源
} catch {
}
}
关键设计模式:先置空,再释放 。将 this.capturer 先设为 null 再执行释放操作,这样可以防止在异步释放过程中,其他方法(如 readAudioLoop 中的循环检查)访问到正在释放的采集器实例。
stop() 和 release() 分别包裹在独立的 try-catch 中,即使 stop() 抛出异常(例如采集器已处于停止状态),release() 仍然可以执行。这种独立异常处理模式确保每个步骤的失败不影响其他步骤。
6.2 releaseEngine:引擎释放
typescript
private releaseEngine(): void {
if (this.engine === null) {
return;
}
try {
this.engine.shutdown(); // 关闭引擎
} catch {
}
this.engine = null;
this.sessionId = ''; // 同时清理 sessionId
}
releaseEngine() 使用 shutdown() 而非 destroy,因为引擎实例可能被多个会话复用。shutdown() 会释放引擎占用的系统服务连接,但不一定会销毁引擎对象本身。
注意 sessionId 的清理:releaseCapturer() 只释放采集器,不清理 sessionId;releaseEngine() 则同时清理 engine 引用和 sessionId。这是因为 sessionId 是引擎会话的唯一标识,引擎释放后 sessionId 不再有意义。
七、BusinessError 格式化
鸿蒙系统 API 的异常通常以 BusinessError 类型抛出,包含 code 和 message 两个字段。formatError 方法将这些原始错误转化为用户友好的错误消息:
typescript
private formatError(error: BusinessError, fallback: string): string {
let message: string = fallback;
if (error.message !== '') {
message = error.message; // 优先使用原始错误消息
}
if (error.code > 0) {
return message + ',错误码:' + error.code.toString(); // 追加错误码
}
return message;
}
格式化策略
| 情况 | 输出示例 |
|---|---|
| 仅有 fallback(error.message 为空,code ≤ 0) | '语音识别启动失败,请稍后重试' |
| 有原始消息(code ≤ 0) | 'create engine failed' |
| 有原始消息 + 错误码 | 'create engine failed,错误码:1001' |
| 仅有错误码(message 为空,code > 0) | '语音识别启动失败,请稍后重试,错误码:1001' |
这种格式化方式让开发者既能获得原始错误信息用于调试,又能让最终用户看到友好的降级提示。错误码的保留也方便了后续的日志分析和问题排查。
八、回调驱动的状态转换
状态机并非完全由 VoiceRecognitionService 自身控制------RecognitionListener 的异步回调也会触发状态转换。这些回调在 createListener 方法中定义:
typescript
private createListener(callbacks: VoiceRecognitionCallbacks): speechRecognizer.RecognitionListener {
const service: VoiceRecognitionService = this;
return {
onResult(sessionId: string, result: speechRecognizer.SpeechRecognitionResult): void {
if (result.result !== '') {
callbacks.onText(result.result, result.isFinal || result.isLast);
}
if (result.isLast) {
service.recording = false; // → PROCESSING
callbacks.onListeningChange(false);
}
},
onComplete(sessionId: string, eventMessage: string): void {
service.recording = false;
callbacks.onStatus('识别完成');
callbacks.onListeningChange(false);
service.releaseEngine(); // → IDLE(可复用)
},
onError(sessionId: string, errorCode: number, errorMessage: string): void {
service.recording = false;
callbacks.onError('语音识别失败,请重新尝试');
callbacks.onListeningChange(false);
service.releaseCapturer(); // 释放采集器
service.releaseEngine(); // 释放引擎
}
};
}
8.1 onComplete:引擎可复用的清理
当 onComplete 被触发时,表示引擎已完成所有音频的识别处理。此时的清理操作是:
- 设置
recording = false - 调用
releaseEngine()释放引擎资源 - 不释放采集器 ------采集器已在
stop()中提前释放
这种清理方式意味着:调用 onComplete 后,服务进入 IDLE 状态,但引擎已被释放。如果需要再次识别,必须通过 start() 重新创建引擎。但请注意,releaseEngine() 仅调用 shutdown(),而非销毁 engine 对象,因此在某些实现中引擎实例仍可被重新初始化。
8.2 onError:全量重置
当 onError 被触发(引擎内部错误或服务端异常),清理操作更为激进:
- 设置
recording = false - 调用
releaseCapturer()释放音频采集器 - 调用
releaseEngine()释放引擎 - 两个资源都释放,确保回到完全干净的 IDLE 状态
与 onComplete 的最大区别在于:onError 同时释放两个资源。这是因为错误发生时,引擎和采集器的状态都可能是不确定的,单独释放其中一个可能导致另一半挂起。
8.3 onComplete vs onError 对比
| 维度 | onComplete | onError |
|---|---|---|
| 触发条件 | 正常完成识别 | 引擎内部错误 |
| 释放采集器 | 否(已在 stop 中释放) | 是 |
| 释放引擎 | 是(shutdown) | 是(shutdown) |
| 重置 recording | 是 | 是 |
| 再次识别 | 需完整 start(含引擎创建) | 需完整 start(含引擎创建) |
| 用户体验 | "识别完成" | "语音识别失败,请重新尝试" |
九、音频读取循环:运行中的状态检查
readAudioLoop 是语音识别的主循环,它在 RECORDING 状态下持续从麦克风读取 PCM 数据:
typescript
private async readAudioLoop(callbacks: VoiceRecognitionCallbacks): Promise<void> {
while (this.recording && this.capturer !== null && this.engine !== null) {
try {
const buffer: ArrayBuffer = await this.capturer.read(AUDIO_CHUNK_SIZE, true);
if (this.recording && this.engine !== null && buffer.byteLength === AUDIO_CHUNK_SIZE) {
this.engine.writeAudio(this.sessionId, new Uint8Array(buffer));
}
} catch {
if (this.recording) {
callbacks.onError('麦克风采集失败,请重新尝试');
callbacks.onListeningChange(false);
await this.destroy();
}
return;
}
}
}
循环守卫
while 条件中包含三个守卫,任何一个为 false 时循环自动退出:
| 守卫 | 含义 | 何时变为 false |
|---|---|---|
this.recording |
是否在录音中 | stop()、destroy()、onResult.isLast、onComplete、onError |
this.capturer !== null |
采集器是否存在 | releaseCapturer() |
this.engine !== null |
引擎是否存在 | releaseEngine() |
这种多层守卫 设计确保循环在服务被停止或资源被释放时能够及时退出,而不会在已释放的资源上执行 read() 或 writeAudio()。
读取异常处理
当 capturer.read() 抛出异常时(例如麦克风被其他应用抢占),catch 块会判断当前是否仍在录音:
- 如果
recording为true:说明是非预期的异常,通过回调通知调用方并执行destroy()进行完整清理 - 如果
recording为false:说明是正常停止过程中由循环自动退出导致的异常,直接return即可
这又是一个防御性编程的典型示例------同样的异常,根据当前状态做出不同的处理决策。
十、重新启动逻辑
语音识别的重新启动并不需要特殊方法,它直接复用已有的 start() → destroy() → start() 模式:
场景一:正常完成后的重新启动
用户点击"开始" → start() → 录音 → onComplete → releaseEngine → IDLE
用户再次点击"开始" → start() → 创建新引擎 → 录音 → ...
onComplete 虽然释放了引擎(通过 releaseEngine()),但将 recording 设为 false,服务回到 IDLE 状态。下一次 start() 调用时,状态守卫不拦截,服务重新创建引擎和采集器,开始新一轮识别。
场景二:异常后的重新启动
用户点击"开始" → start() → 发生异常 → catch → destroy() → IDLE
用户再次点击"开始" → start() → 创建新引擎 → 录音 → ...
异常发生后,destroy() 确保所有资源被完全释放。下一次 start() 从零开始,不存在残留资源干扰新会话的问题。
场景三:强制停止后的重新启动
用户点击"开始" → 录音中
用户突然离开页面 → aboutToDisappear → destroy() → IDLE
用户回到页面 → start() → 正常识别
在组件生命周期中调用 destroy() 是防止资源泄漏的最后一层保障。以 Index 组件的清理代码为例:
typescript
aboutToDisappear(): void {
// ... 其他清理
this.voiceRecognitionService.destroy();
}
这种设计确保无论用户如何操作(正常停止、异常退出、强制返回),服务始终能够回到 IDLE 状态,为下一次使用做好准备。
十一、状态机总结
将 VoiceRecognitionService 的状态管理抽象为一张完整的决策表:
| 操作 | 前置条件(状态守卫) | 执行内容 | 目标状态 |
|---|---|---|---|
start() |
recording === false |
创建引擎+采集器,启动录音 | RECORDING |
stop() |
recording === true |
releaseCapturer → engine.finish | IDLE |
destroy() |
无(任意状态可调用) | releaseCapturer → engine.cancel → releaseEngine | IDLE |
onResult(isLast) |
引擎回调 | recording = false | PROCESSING |
onComplete |
引擎回调 | recording = false → releaseEngine | IDLE(引擎已释放) |
onError |
引擎回调 | recording = false → releaseCapturer → releaseEngine | IDLE(全量释放) |
readAudioLoop 异常 |
capturer.read 失败 |
if recording → destroy,否则 return | IDLE 或维持原状态 |
核心设计原则
- 状态守卫先行 :每个公开方法(
start、stop、destroy)在操作前先检查当前状态,非法操作直接返回 - 先置空再释放:资源释放前先将引用置为 null,避免异步竞态
- 优雅 vs 强制 :正常停止用
finish(等待完成),异常恢复用cancel(立即终止) - 差异化清理 :
onComplete只 releaseEngine,onError同时 release 两个资源 - 统一入口恢复 :无论何种异常,最终都通过
destroy()回到 IDLE 状态,重新启动只需再次调用start()
十二、与其他服务的模式对比
状态机的设计思路在鸿蒙的其他系统能力服务中同样适用:
| 服务 | 状态机 | 状态守卫 | 资源释放 |
|---|---|---|---|
| 语音识别 | IDLE ↔ RECORDING ↔ PROCESSING | recording 布尔值 |
releaseCapturer + releaseEngine |
| 相机拍照 | IDLE ↔ CAPTURING | recognizing 布尔值 |
startAbilityForResult 系统管理 |
| HTTP 请求 | 会话创建 → 请求 → 销毁 | 无需守卫 | try-finally-destroy |
| 视频播放 | IDLE ↔ PREPARED ↔ PLAYING | VideoController 状态 | controller.release |
与相机拍照相比,语音识别的状态管理更为复杂------相机拍照是一次性操作(启动相机→获取结果→结束),而语音识别涉及持续的音频流处理、异步回调、以及多资源管理。这也解释了为什么语音识别需要更加严谨的状态机设计。
总结
本文深入解析了 VoiceRecognitionService 中的异步状态机设计,涵盖了语音识别的完整状态管理方案:
| 知识点 | 实现方式 |
|---|---|
| 四状态状态机 | IDLE → RECORDING → PROCESSING → ERROR → IDLE |
| 状态守卫 | recording 布尔值拦截非法操作 |
| 优雅终止 | engine.finish(sessionId) 完成会话后触发 onComplete |
| 强制取消 | engine.cancel(sessionId) 立即终止,不触发回调 |
| 资源释放 | releaseCapturer(stop+release)+ releaseEngine(shutdown+null) |
| 错误格式化 | formatError 提取 BusinessError 的 code 和 message |
| 重新启动 | destroy() → IDLE → start() 创建新会话 |
| 差异化清理 | onComplete 轻量释放 vs onError 全量重置 |
下一篇: 第 4.7 篇将介绍服务卡片开发,学习如何通过 form_config 和 FormExtensionAbility 让用户在桌面上直接查看创作进度。
参考源码
本文所有代码均来自项目文件:
products/default/src/main/ets/services/VoiceRecognitionService.ets--- 语音识别服务完整实现,包含状态管理、资源释放、回调处理和异常格式化