解析鸿蒙 TextToSpeechPlugin:引擎创建、监听器和 stop 控制
2026/6/14 16:46:07 网站建设 项目流程

适合谁看

  • 想读懂TextToSpeechPlugin.ets的开发者

  • 想写命令型鸿蒙原生插件的人

  • 想理解 TTS 引擎生命周期管理的人

问题背景

TTS 插件常见的问题不是"能不能播报",而是:

  • 引擎是否重复创建

  • 完成和停止如何区分

  • 页面多次点击播报按钮时状态怎么收口

  • pendingResult 挂起后怎么回收

这些都决定了插件是否可维护。

项目中的真实场景

食界探味的 TTS 插件位于:

  • app/ohos/entry/src/main/ets/plugins/TextToSpeechPlugin.ets

Flutter 侧对应:

  • app/lib/core/platform/text_to_speech_channel.dart

插件提供两个方法:

方法

作用

类型

speak

播报文本

命令型(异步,等待完成)

stop

停止播报

命令型(立即返回)

核心实现

一、插件结构——3 个关键字段

export default class TextToSpeechPlugin implements FlutterPlugin, MethodCallHandler { private channel: MethodChannel | null = null; // Flutter 通信通道 private ttsEngine: textToSpeech.TextToSpeechEngine | null = null; // TTS 引擎 private pendingResult: MethodResult | null = null; // 挂起的回调结果

这 3 个字段构成了整个插件的核心状态:

字段

职责

生命周期

channel

和 Flutter 通信

插件 attach 时创建,detach 时清空

ttsEngine

鸿蒙 TTS 引擎

首次 speak 时创建,可复用

pendingResult

一次播报的回调句柄

speak 时设置,完成/停止/出错时回收

pendingResult是整个插件最关键的设计——它把"一次播报命令"的生命周期收成了一个可追踪的对象。

二、插件生命周期——attach 和 detach

onAttachedToEngine(binding: FlutterPluginBinding): void { this.channel = new MethodChannel( binding.getBinaryMessenger(), 'com.foodvoyage.text_to_speech' ); this.channel.setMethodCallHandler(this); } onDetachedFromEngine(binding: FlutterPluginBinding): void { if (this.channel) { this.channel.setMethodCallHandler(null); // 清空处理器 } this.shutdownEngine(); // 释放 TTS 引擎 }

onAttachedToEngine:Flutter 引擎启动时调用,创建 MethodChannel 并注册处理器。

onDetachedFromEngine:Flutter 引擎销毁时调用,做两件事:

  1. 清空 MethodChannel 处理器,防止野指针调用

  2. 释放 TTS 引擎,归还音频资源

这两个方法是鸿蒙 Flutter 插件的标准生命周期,必须正确实现。

三、方法分发——onMethodCall

onMethodCall(call: MethodCall, result: MethodResult): void { switch (call.method) { case 'speak': this.handleSpeak(call, result); break; case 'stop': this.handleStop(result); break; default: result.notImplemented(); // 未知方法返回 notImplemented break; } }

简洁的路由分发。未知方法返回result.notImplemented(),让 Flutter 侧能收到明确的错误,而不是挂起。

四、handleSpeak()——命令型方法的完整流程

private async handleSpeak(call: MethodCall, result: MethodResult): Promise<void> { // 1. 提取参数 const text = call.argument('text') as string; // 2. 参数校验(尽早返回) if (!text || text.length === 0) { result.error('INVALID_ARGUMENT', '播报文本不能为空', null); return; } // 3. 保存 pendingResult this.pendingResult = result; // 4. 创建引擎 + 播报 try { await this.createEngine(); this.setupListenerAndSpeak(text); } catch (err) { this.pendingResult = null; const error = err as BusinessError; result.error('TTS_ERROR', `文本转语音启动失败: ${error.message}`, null); } }

关键设计点:

参数校验在最前面— TTS 属于命令型能力,参数错误应该尽早返回,而不是拖到引擎层。

pendingResult 在校验后保存— 如果参数为空,直接result.error()返回,不保存 pendingResult。

异常时清理 pendingResultcatch块里先把this.pendingResult = null,再调result.error(),避免重复回收。

五、引擎创建——懒加载 + 单例复用

private createEngine(): Promise<void> { return new Promise((resolve, reject) => { // 单例复用:已创建则直接返回 if (this.ttsEngine) { resolve(); return; } const initParams: textToSpeech.CreateEngineParams = { language: 'zh-CN', person: 0, online: 1, extraParams: { 'style': 'interaction-broadcast', // 广播风格 'locate': 'CN', 'name': 'EngineName' } }; textToSpeech.createEngine(initParams, (err, engine) => { if (!err) { console.info(TAG, 'TTS engine created successfully'); this.ttsEngine = engine; resolve(); } else { console.error(TAG, `Failed to create TTS engine: ${err.message}`); reject(err); } }); }); }

懒加载— 只在第一次调用speak时创建引擎,不在插件初始化时创建。这避免了用户从未使用 TTS 时浪费资源。

单例复用— 已创建则直接返回。TTS 引擎创建成本较高(需要初始化音频通道、加载语音模型),复用能显著提升性能。

Promise 封装— 鸿蒙textToSpeech.createEngine是回调式 API,用 Promise 包装后可以 async/await,方便handleSpeak串行调用。

引擎参数说明:

参数

为什么选这个值

language

zh-CN

中文用户

person

0

默认发音人

online

1

在线模式音质更好

style

interaction-broadcast

广播风格,适合推荐场景

locate

CN

中国区服务器

六、监听器——5 个回调的职责分工

setupListenerAndSpeak()注册了 5 个监听器回调:

const speakListener: textToSpeech.SpeakListener = { onStart: (requestId, response) => { console.info(TAG, `onStart requestId: ${requestId}`); // 什么都不做,只是记录日志 }, onComplete: (requestId, response) => { console.info(TAG, `onComplete requestId: ${requestId}`); if (this.pendingResult) { this.pendingResult.success(null); // 通知 Flutter:播报完成 this.pendingResult = null; // 清空挂起结果 } }, onStop: (requestId, response) => { console.info(TAG, `onStop requestId: ${requestId}`); if (this.pendingResult) { this.pendingResult.success(null); // 通知 Flutter:停止完成 this.pendingResult = null; } }, onData: (requestId, audio, response) => { console.info(TAG, `onData requestId: ${requestId}, sequence: ${response.sequence}`); // 音频数据流,当前只记日志 }, onError: (requestId, errorCode, errorMessage) => { console.error(TAG, `onError code: ${errorCode}, msg: ${errorMessage}`); if (this.pendingResult) { this.pendingResult.error('TTS_ERROR', errorMessage, null); // 通知 Flutter:出错 this.pendingResult = null; } } };

5 个回调的职责分工:

回调

触发时机

处理方式

重要性

onStart

播报开始

只记日志

onComplete

播报自然完成

回传 success + 清空 pendingResult

onStop

被用户主动停止

回传 success + 清空 pendingResult

onData

音频数据流

只记日志

onError

出错

回传 error + 清空 pendingResult

关键点:onComplete 和 onStop 都要回收 pendingResult。用户手动停止时,Flutter 侧的await speak()也需要返回,不能挂起。

七、pendingResult 的生命周期——一次播报的完整追踪

pendingResult是整个插件最关键的设计。它追踪的是"一次播报命令"的完整生命周期:

handleSpeak() → this.pendingResult = result ← 保存回调句柄 → createEngine() → setupListenerAndSpeak() → speak(text, params) ← 发起播报 │ ├─ onComplete: ← 播报自然完成 │ pendingResult.success(null) │ pendingResult = null ← 生命周期结束 │ ├─ onStop: ← 用户手动停止 │ pendingResult.success(null) │ pendingResult = null ← 生命周期结束 │ └─ onError: ← 出错 pendingResult.error(...) pendingResult = null ← 生命周期结束

这个设计的好处:

  1. Flutter 侧的 await 一定会返回— 无论是完成、停止还是出错,pendingResult 都会被回收

  2. 不会出现回调泄漏— 每次播报都有明确的结束点

  3. 多次播报不会冲突— 新的 speak 会覆盖旧的 pendingResult

但有一个潜在问题:如果用户快速连续点击播报按钮,旧的 pendingResult 会被覆盖,Flutter 侧的旧 await 会永远挂起。当前页面层通过_isSpeaking状态防止了这种情况。

八、handleStop()——主动停止的完整逻辑

private handleStop(result: MethodResult): void { try { if (this.ttsEngine) { this.ttsEngine.stop(); } result.success(null); // 立即返回成功 } catch (err) { const error = err as BusinessError; result.error('TTS_ERROR', `停止播报失败: ${error.message}`, null); } }

注意 handleStop 和 handleSpeak 的区别:

维度

handleSpeak

handleStop

返回时机

播报完成后才返回

立即返回

pendingResult

需要等监听器回收

直接 result.success()

异步性

async

同步

stop 是同步返回的——调用后立即告诉 Flutter"停止指令已发送"。实际的停止效果由鸿蒙 TTS 引擎异步处理,停止完成后触发onStop回调。

这意味着 Flutter 侧调用stop()后,不需要await等待停止完成,可以立即更新 UI。

九、播报参数——setupListenerAndSpeak() 的后半段

const extraParam: Record<string, Object> = { 'queueMode': 0, // 不排队,新播报直接开始 'speed': 1, // 正常语速 'volume': 2, // 音量 'pitch': 1, // 正常音调 'languageContext': 'zh-CN', 'audioType': 'pcm', 'soundChannel': 3, 'playType': 1 }; const speakParams: textToSpeech.SpeakParams = { requestId: `tts_${Date.now()}`, // 唯一标识,用于追踪 extraParams: extraParam }; this.ttsEngine.speak(text, speakParams);

关键参数:

参数

说明

queueMode

0

不排队——如果正在播报,新播报直接打断旧的

speed

1

正常语速(1.0 倍)

volume

2

音量级别

pitch

1

正常音调

requestId

tts_时间戳

唯一标识,用于日志追踪和回调匹配

requestId用时间戳生成,保证每次播报都有唯一标识。在调试时可以通过 requestId 追踪一次播报的完整生命周期。

十、引擎销毁——shutdownEngine()

private shutdownEngine(): void { try { if (this.ttsEngine) { this.ttsEngine.shutdown(); this.ttsEngine = null; console.info(TAG, 'TTS engine shutdown'); } } catch (err) { console.error(TAG, `shutdown error: ${JSON.stringify(err)}`); } }

shutdown 做两件事:

  1. this.ttsEngine.shutdown()— 释放鸿蒙 TTS 引擎占用的音频通道和内存

  2. this.ttsEngine = null— 清空引用,方便后续重新创建

shutdown 本身也包了 try-catch,防止引擎状态异常时导致插件崩溃。

在鸿蒙设备上,TTS 引擎是重资源。如果不 shutdown,引擎会一直占用音频通道,其他应用可能无法使用 TTS/ASR 功能。

关键代码位置

文件

作用

app/ohos/entry/src/main/ets/plugins/TextToSpeechPlugin.ets

鸿蒙 TTS 插件(本文核心)

app/lib/core/platform/text_to_speech_channel.dart

Flutter TTS 通道

代码结构全景图

TextToSpeechPlugin │ ├─ 字段 │ ├─ channel: MethodChannel ← Flutter 通信 │ ├─ ttsEngine: TtsEngine ← 鸿蒙 TTS 引擎 │ └─ pendingResult: MethodResult ← 播报回调句柄 │ ├─ 生命周期 │ ├─ onAttachedToEngine() ← 创建 channel │ └─ onDetachedFromEngine() ← 清空 channel + shutdown 引擎 │ ├─ 方法分发 │ └─ onMethodCall() │ ├─ 'speak' → handleSpeak() │ └─ 'stop' → handleStop() │ ├─ handleSpeak() │ ├─ 参数校验(空文本 → error) │ ├─ 保存 pendingResult │ ├─ createEngine()(懒加载 + 单例) │ └─ setupListenerAndSpeak() │ ├─ 注册 5 个监听器 │ │ ├─ onStart → 日志 │ │ ├─ onComplete → success + 清空 pending │ │ ├─ onStop → success + 清空 pending │ │ ├─ onData → 日志 │ │ └─ onError → error + 清空 pending │ └─ speak(text, params) │ ├─ handleStop() │ └─ ttsEngine.stop() → 立即 success │ └─ shutdownEngine() └─ ttsEngine.shutdown() + null

常见坑

  • 每次播报都重建引擎— 开销大,应该懒加载 + 单例复用

  • stop 不回收 pendingResult— Flutter 侧的 await 永远挂起

  • onComplete 和 onStop 只处理一个— 两种路径都要回收 pendingResult

  • 异常时不清理 pendingResult— 导致重复回收或泄漏

  • 没有 shutdownEngine— 鸿蒙端音频通道和内存不释放

  • queueMode 设为排队— 多次点击播报会排队执行,体验差

  • requestId 不唯一— 调试时无法区分不同播报的生命周期

  • onDetachedFromEngine 不调 shutdownEngine— 插件卸载后引擎还在运行

可复用模板

鸿蒙命令型插件模板

export default class CommandPlugin implements FlutterPlugin, MethodCallHandler { private channel: MethodChannel | null = null; private engine: SomeEngine | null = null; private pendingResult: MethodResult | null = null; onAttachedToEngine(binding: FlutterPluginBinding): void { this.channel = new MethodChannel(binding.getBinaryMessenger(), 'com.yourapp.command'); this.channel.setMethodCallHandler(this); } onDetachedFromEngine(binding: FlutterPluginBinding): void { this.channel?.setMethodCallHandler(null); this.shutdownEngine(); } onMethodCall(call: MethodCall, result: MethodResult): void { switch (call.method) { case 'execute': this.handleExecute(call, result); break; case 'cancel': this.handleCancel(result); break; default: result.notImplemented(); } } private async handleExecute(call: MethodCall, result: MethodResult): Promise<void> { const param = call.argument('param') as string; if (!param) { result.error('EMPTY', '参数为空', null); return; } this.pendingResult = result; try { await this.ensureEngine(); this.setupListenerAndExecute(param); } catch (err) { this.pendingResult = null; result.error('ERROR', `${err.message}`, null); } } private handleCancel(result: MethodResult): void { this.engine?.cancel(); result.success(null); } private async ensureEngine(): Promise<void> { if (this.engine) return; // 创建引擎... } private setupListenerAndExecute(param: string): void { this.engine?.setListener({ onComplete: () => { this.pendingResult?.success(null); this.pendingResult = null; }, onCancel: () => { this.pendingResult?.success(null); this.pendingResult = null; }, onError: (_, msg) => { this.pendingResult?.error('ERROR', msg); this.pendingResult = null; }, }); this.engine?.execute(param); } private shutdownEngine(): void { this.engine?.shutdown(); this.engine = null; } }

pendingResult 回收检查清单

每个命令型方法必须检查: □ 参数校验失败时是否清理了 pendingResult? □ 引擎创建失败时是否清理了 pendingResult? □ onComplete 时是否回收了 pendingResult? □ onCancel/onStop 时是否回收了 pendingResult? □ onError 时是否回收了 pendingResult? □ 新命令进来时是否覆盖了旧的 pendingResult?

本篇总结

TextToSpeechPlugin的重点在于生命周期控制,而不是 API 数量。核心设计是:

  1. pendingResult 追踪一次播报的完整生命周期— speak 时保存,完成/停止/出错时回收

  2. 引擎懒加载 + 单例复用— 首次 speak 时创建,后续复用

  3. 5 个监听器各有分工— onComplete/onStop/onError 都要回收 pendingResult

  4. stop 是同步返回的— 不需要 Flutter 侧 await 等待

  5. shutdownEngine 在 onDetachedFromEngine 时调用— 确保引擎资源释放

这种"命令型能力"的插件结构很稳定,可以复用到其他鸿蒙原生能力的接入中。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询