前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

“创建容易销毁难”——这句话在资源管理领域特别适用。语音识别引擎占用了麦克风、网络连接、内存等多种系统资源,如果不正确释放,轻则内存泄漏,重则麦克风被锁死,其他App都用不了。

flutter_speech的destroyEngine方法只有十几行代码,但它是整个插件最后的防线。不管前面的流程多么完美,如果销毁没做好,都会留下隐患。

今天我们来看看这十几行代码背后的设计考量。

💡 本文对应源码FlutterSpeechPlugin.etsdestroyEngine方法(第250-263行)和onDetachedFromEngine方法(第38-43行)。
请添加图片描述

一、destroyEngine 方法实现解析

1.1 完整源码

private destroyEngine(): void {
  try {
    if (this.asrEngine) {
      if (this.isListening) {
        this.asrEngine.cancel(this.sessionId);
      }
      this.asrEngine.shutdown();
      this.asrEngine = null;
      this.isListening = false;
    }
  } catch (e) {
    console.error(TAG, `destroyEngine error: ${JSON.stringify(e)}`);
  }
}

1.2 逐行解析

第1行if (this.asrEngine)

  • 检查引擎是否存在。如果已经销毁过了(asrEngine=null),直接跳过
  • 这保证了多次调用destroyEngine是安全的

第2-4行if (this.isListening) { this.asrEngine.cancel(this.sessionId); }

  • 如果正在识别中,先取消当前识别
  • 为什么用cancel而不是finish?因为销毁引擎时不需要最终结果了,cancel更快

第5行this.asrEngine.shutdown()

  • 关闭引擎,释放所有底层资源
  • 这是Core Speech Kit的引擎关闭API

第6行this.asrEngine = null

  • 将引用置为null,帮助GC回收
  • 也防止后续代码误用已销毁的引擎

第7行this.isListening = false

  • 重置监听状态

1.3 为什么整个方法被try-catch包裹

try {
  // ...
} catch (e) {
  console.error(TAG, `destroyEngine error: ${JSON.stringify(e)}`);
}

销毁方法绝对不能抛异常。原因:

  1. destroyEngine通常在清理阶段调用(如onDetachedFromEngine
  2. 如果清理阶段抛异常,可能导致后续的清理逻辑被跳过
  3. 引擎可能已经处于异常状态,shutdown可能失败,但我们仍然需要置null

📌 设计原则:销毁/清理方法应该是"尽力而为"的——尽量释放资源,但即使失败也不能影响程序的其他部分。

二、shutdown 与 cancel 的调用顺序

2.1 为什么要先cancel再shutdown

if (this.isListening) {
  this.asrEngine.cancel(this.sessionId);  // 先cancel
}
this.asrEngine.shutdown();                 // 再shutdown

如果不先cancel就直接shutdown,可能会出现以下问题:

场景 不先cancel 先cancel
正在识别中 shutdown可能失败或行为未定义 cancel停止识别,shutdown正常关闭
音频采集中 麦克风可能不会被释放 cancel释放麦克风,shutdown释放引擎
回调进行中 回调可能在shutdown后触发(野指针) cancel停止回调,shutdown安全关闭

2.2 调用顺序图

destroyEngine()
    │
    ├── isListening == true?
    │   ├── 是 → cancel(sessionId) → 停止音频采集,停止回调
    │   └── 否 → 跳过
    │
    ├── shutdown() → 释放引擎资源,断开AI服务连接
    │
    ├── asrEngine = null → 清除引用
    │
    └── isListening = false → 重置状态

2.3 如果cancel失败怎么办

try {
  if (this.isListening) {
    this.asrEngine.cancel(this.sessionId);  // 可能失败
  }
  this.asrEngine.shutdown();  // 仍然会执行
} catch (e) {
  // cancel或shutdown失败都会被捕获
}
// asrEngine = null 和 isListening = false 在catch外面?

等等,仔细看源码,asrEngine = nullisListening = false是在try块内部的。如果cancel或shutdown抛异常,这两行不会执行。

这是一个潜在的问题:如果shutdown失败,asrEngine不会被置null,下次调用destroyEngine时会再次尝试shutdown。不过由于整个方法被try-catch包裹,不会导致崩溃。

🤔 改进建议:可以把置null和状态重置放到finally块中,确保无论如何都会执行:

// 改进版本(flutter_speech未采用,仅供参考)
private destroyEngine(): void {
  try {
    if (this.asrEngine) {
      if (this.isListening) {
        this.asrEngine.cancel(this.sessionId);
      }
      this.asrEngine.shutdown();
    }
  } catch (e) {
    console.error(TAG, `destroyEngine error: ${JSON.stringify(e)}`);
  } finally {
    this.asrEngine = null;
    this.isListening = false;
  }
}

三、onDetachedFromEngine 中的清理逻辑

3.1 源码

onDetachedFromEngine(binding: FlutterPluginBinding): void {
  if (this.channel != null) {
    this.channel.setMethodCallHandler(null);
  }
  this.destroyEngine();
}

3.2 清理顺序

onDetachedFromEngine
    │
    ├── 1. 取消MethodCallHandler注册
    │   └── channel.setMethodCallHandler(null)
    │       防止在销毁过程中收到新的方法调用
    │
    └── 2. 销毁引擎
        └── destroyEngine()
            ├── cancel当前识别(如果有)
            ├── shutdown引擎
            ├── 置null
            └── 重置状态

3.3 为什么先取消Handler再销毁引擎

// 先取消Handler
this.channel.setMethodCallHandler(null);

// 再销毁引擎
this.destroyEngine();

如果顺序反过来:

  1. 先destroyEngine → asrEngine = null
  2. 此时如果Dart层发来一个"speech.listen"调用
  3. onMethodCall被触发,但asrEngine已经是null
  4. 返回ERROR_ENGINE_NOT_INITIALIZED错误

虽然不会崩溃,但会产生一个不必要的错误日志。先取消Handler可以避免这种情况。

3.4 channel本身需要置null吗

flutter_speech的当前实现没有将channel置为null:

onDetachedFromEngine(binding: FlutterPluginBinding): void {
  if (this.channel != null) {
    this.channel.setMethodCallHandler(null);
  }
  this.destroyEngine();
  // 注意:没有 this.channel = null;
}

这是可以接受的,因为setMethodCallHandler(null)已经断开了通信链路。但如果要更严格,可以加上:

onDetachedFromEngine(binding: FlutterPluginBinding): void {
  if (this.channel != null) {
    this.channel.setMethodCallHandler(null);
    this.channel = null;  // 更严格的清理
  }
  this.destroyEngine();
}

四、内存泄漏防范:引用置空与状态重置

4.1 需要置空的引用

flutter_speech中有以下需要管理的引用:

引用 类型 置空位置 不置空的后果
channel MethodChannel onDetachedFromEngine(未置空) 轻微泄漏
asrEngine SpeechRecognitionEngine destroyEngine 严重泄漏(引擎资源)
abilityContext UIAbilityContext onDetachedFromAbility 严重泄漏(系统资源)

4.2 引擎资源泄漏的表现

如果asrEngine不置空:

  1. 麦克风被占用:其他App无法录音
  2. 网络连接保持:持续消耗流量和电量
  3. 内存持续增长:引擎内部的缓冲区不会释放
  4. AI服务连接不释放:可能影响系统其他AI功能

4.3 状态重置的重要性

this.isListening = false;
this.lastTranscription = '';  // flutter_speech没有在destroyEngine中重置这个

isListening必须重置,否则下次创建引擎后,startListening的防重入逻辑会误判。

lastTranscription在destroyEngine中没有重置,但在startListening中会重置(this.lastTranscription = ''),所以不会有问题。

4.4 GC与引用链

FlutterSpeechPlugin实例
    │
    ├── channel → MethodChannel → BinaryMessenger → FlutterEngine
    │
    ├── asrEngine → SpeechRecognitionEngine → AI Service → 麦克风
    │
    └── abilityContext → UIAbilityContext → Ability → WindowStage

如果FlutterSpeechPlugin实例持有这些引用不释放,整条引用链上的对象都无法被GC回收。这就是为什么每个引用都需要在适当的时机置null。

五、与 Android speech.destroy 的对比

5.1 Android的销毁实现

// Android
private void destroyEngine() {
    if (speechRecognizer != null) {
        speechRecognizer.destroy();
        speechRecognizer = null;
    }
    isListening = false;
}

5.2 对比

对比项 Android OpenHarmony
销毁API destroy() shutdown()
先cancel 没有显式cancel ✅ 先cancel再shutdown
异常处理 无try-catch ✅ 有try-catch
引用置空 ✅ = null ✅ = null
状态重置 ✅ isListening = false ✅ isListening = false

OpenHarmony的实现比Android更健壮

  1. 多了先cancel的步骤
  2. 多了try-catch保护

5.3 Dart层的destroy方法

flutter_speech的Dart层没有暴露destroy方法给用户。引擎的销毁完全由原生端的生命周期管理:

用户不需要手动销毁
    │
    ├── 正常退出 → onDetachedFromEngine → destroyEngine
    │
    ├── 热重载 → onDetachedFromEngine → destroyEngine → onAttachedToEngine
    │
    └── 异常退出 → 系统回收资源

但在原生端的onMethodCall中,有一个speech.destroy的处理:

case "speech.destroy":
  this.destroyEngine();
  result.success(true);
  break;

这是给Dart层预留的接口,虽然当前的Dart API没有暴露这个方法,但如果需要,可以通过MethodChannel直接调用。

六、资源释放的完整时序

6.1 正常退出

App退出
    │
    ├── onDetachedFromAbility()
    │   └── abilityContext = null
    │
    └── onDetachedFromEngine()
        ├── channel.setMethodCallHandler(null)
        └── destroyEngine()
            ├── cancel(如果在识别中)
            ├── shutdown()
            ├── asrEngine = null
            └── isListening = false

6.2 热重载

热重载触发
    │
    ├── onDetachedFromAbility()
    │   └── abilityContext = null
    │
    ├── onDetachedFromEngine()
    │   ├── channel.setMethodCallHandler(null)
    │   └── destroyEngine()
    │
    ├── (重新加载)
    │
    ├── onAttachedToEngine()
    │   ├── 创建新的MethodChannel
    │   └── 注册MethodCallHandler
    │
    └── onAttachedToAbility()
        └── 获取新的abilityContext

热重载时,旧的资源会被完全释放,然后重新创建。这就是为什么destroyEngine必须做到位——如果旧引擎没有正确释放,新引擎可能无法创建(资源冲突)。

6.3 异常退出

如果App被系统强制杀死(如OOM),onDetachedFromEngine可能不会被调用。这种情况下,系统会自动回收进程的所有资源,包括麦克风和网络连接。所以不需要担心。

七、资源管理最佳实践

7.1 "谁创建谁释放"原则

资源 创建位置 释放位置
MethodChannel onAttachedToEngine onDetachedFromEngine
asrEngine activate destroyEngine
abilityContext onAttachedToAbility onDetachedFromAbility
监听器 setupListener (随引擎一起释放)

7.2 释放顺序

释放顺序应该和创建顺序相反

创建顺序:Engine → Ability → Channel → Engine(ASR) → Listener
释放顺序:Listener → Engine(ASR) → Channel → Ability → Engine

7.3 防御性编程检查清单

  • 所有引用在释放后置为null
  • 所有状态标志在释放后重置
  • 销毁方法可以安全地多次调用
  • 销毁方法不会抛出异常(try-catch保护)
  • 先停止活动操作再释放资源
  • 生命周期方法中有完整的清理逻辑

7.4 常见的资源泄漏模式

// ❌ 泄漏模式1:只置null不shutdown
this.asrEngine = null;  // 引擎资源没有释放!

// ❌ 泄漏模式2:只shutdown不置null
this.asrEngine.shutdown();  // 引用还在,GC无法回收

// ❌ 泄漏模式3:不处理正在进行的操作
this.asrEngine.shutdown();  // 正在识别中直接shutdown,行为未定义

// ✅ 正确模式:完整的清理流程
if (this.isListening) this.asrEngine.cancel(this.sessionId);
this.asrEngine.shutdown();
this.asrEngine = null;
this.isListening = false;

八、调试资源释放

8.1 日志确认

# 确认销毁流程正确执行
hdc hilog | grep "FlutterSpeechPlugin" | grep -E "onDetached|destroyEngine|shutdown|cancel"

正常退出的日志:

FlutterSpeechPlugin: 🔴 onDetachedFromAbility
FlutterSpeechPlugin: 🔴 onDetachedFromEngine

8.2 内存监控

# 查看App内存使用
hdc shell hidumper -s 12345 --mem  # 12345是进程ID

如果内存持续增长(每次activate/destroy后不回落),说明有泄漏。

8.3 麦克风占用检测

如果销毁后麦克风仍被占用,其他App录音会失败。可以用系统的录音App测试:

  1. 使用flutter_speech识别
  2. 退出flutter_speech
  3. 打开系统录音App
  4. 如果录音正常,说明麦克风已正确释放

总结

本文详细讲解了flutter_speech中引擎销毁与资源释放的实现:

  1. destroyEngine方法:先cancel再shutdown,然后置null和重置状态
  2. 调用顺序:cancel → shutdown → null → reset,顺序不能乱
  3. onDetachedFromEngine:先取消Handler再销毁引擎
  4. 内存泄漏防范:所有引用置null,所有状态重置
  5. 异常保护:try-catch包裹,销毁方法绝不抛异常
  6. 多次调用安全:null检查保证重复调用不会出错

下一篇我们讲MethodChannel双向通信的完整实现——Dart到Native和Native到Dart的所有通信路径。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐