HarmonyOS APP《画伴梦工厂》开发第32篇-语音识别状态管理——开始停止与异常处理

第4.6篇:语音识别状态管理------开始、停止与异常处理

系列 :鸿蒙系统能力与设备协同篇

难度 :⭐⭐⭐ 高级

前置知识 :4.5 语音识别实战

涉及源文件products/default/src/main/ets/services/VoiceRecognitionService.ets


在上一篇中,我们实现了语音识别的完整接入------创建 SpeechRecognitionEngine、配置 AudioCapturer 进行音频采集、通过 RecognitionListener 获取识别结果。然而,一个生产级的语音识别服务不仅要"能跑起来",还需要可靠地管理状态:防止重复启动、在适当的时机释放资源、优雅地结束会话、以及从各种异常中恢复。

本文将深入 VoiceRecognitionService 的内部,剖析其异步状态机设计、资源释放策略、以及异常处理机制。这些模式不仅适用于语音识别,也是鸿蒙中所有长生命周期系统能力(如相机、音频播放、网络连接)的通用最佳实践。


一、为什么需要状态管理?

语音识别涉及多个系统级资源的协作:

  • SpeechRecognitionEngine:语音识别引擎,持有与系统 AI 服务的连接
  • AudioCapturer:音频采集器,占用麦克风硬件
  • 音频读取循环:持续从麦克风读取 PCM 数据并写入引擎
  • RecognitionListener:异步回调,可能在任何时刻触发

如果这些资源的状态管理不当,会出现以下问题:

问题 后果
用户快速点击"开始/停止" 多个音频采集器同时运行,麦克风冲突
页面退出时未停止识别 引擎和采集器资源泄漏,系统服务残留
异常发生后直接重试 旧资源未释放,新资源创建失败
多次调用 destroy 空指针异常或重复释放

解决这些问题的方式,就是引入状态机------让服务在任何时刻都处于一个明确的、可预测的状态,所有操作只在合法的状态下执行。


二、四状态状态机设计

VoiceRecognitionService 内部维护了一个隐式状态机 ,通过 recording 布尔值和对象引用(enginecapturer)的 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()强制取消 ,不会等待引擎完成处理,也不会触发 onCompleteonResult。这在异常处理场景中非常合适------当出现错误时,我们不需要已损坏的识别结果,只需快速释放资源。


六、资源释放: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 类型抛出,包含 codemessage 两个字段。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.isLastonCompleteonError
this.capturer !== null 采集器是否存在 releaseCapturer()
this.engine !== null 引擎是否存在 releaseEngine()

这种多层守卫 设计确保循环在服务被停止或资源被释放时能够及时退出,而不会在已释放的资源上执行 read()writeAudio()

读取异常处理

capturer.read() 抛出异常时(例如麦克风被其他应用抢占),catch 块会判断当前是否仍在录音:

  • 如果 recordingtrue:说明是非预期的异常,通过回调通知调用方并执行 destroy() 进行完整清理
  • 如果 recordingfalse:说明是正常停止过程中由循环自动退出导致的异常,直接 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 或维持原状态

核心设计原则

  1. 状态守卫先行 :每个公开方法(startstopdestroy)在操作前先检查当前状态,非法操作直接返回
  2. 先置空再释放:资源释放前先将引用置为 null,避免异步竞态
  3. 优雅 vs 强制 :正常停止用 finish(等待完成),异常恢复用 cancel(立即终止)
  4. 差异化清理onComplete 只 releaseEngine,onError 同时 release 两个资源
  5. 统一入口恢复 :无论何种异常,最终都通过 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 --- 语音识别服务完整实现,包含状态管理、资源释放、回调处理和异常格式化