解析鸿蒙 TextToSpeechPlugin:引擎创建、监听器和 stop 控制
适合谁看
-
想读懂
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
插件提供两个方法:
|
方法 |
作用 |
类型 |
|---|---|---|
|
|
播报文本 |
命令型(异步,等待完成) |
|
|
停止播报 |
命令型(立即返回) |
核心实现
一、插件结构——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 个字段构成了整个插件的核心状态:
|
字段 |
职责 |
生命周期 |
|---|---|---|
|
|
和 Flutter 通信 |
插件 attach 时创建,detach 时清空 |
|
|
鸿蒙 TTS 引擎 |
首次 speak 时创建,可复用 |
|
|
一次播报的回调句柄 |
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 引擎销毁时调用,做两件事:
-
清空 MethodChannel 处理器,防止野指针调用
-
释放 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。
异常时清理 pendingResult — catch 块里先把 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 串行调用。
引擎参数说明:
|
参数 |
值 |
为什么选这个值 |
|---|---|---|
|
|
|
中文用户 |
|
|
|
默认发音人 |
|
|
|
在线模式音质更好 |
|
|
|
广播风格,适合推荐场景 |
|
|
|
中国区服务器 |
六、监听器——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 个回调的职责分工:
|
回调 |
触发时机 |
处理方式 |
重要性 |
|---|---|---|---|
|
|
播报开始 |
只记日志 |
低 |
|
|
播报自然完成 |
回传 success + 清空 pendingResult |
高 |
|
|
被用户主动停止 |
回传 success + 清空 pendingResult |
高 |
|
|
音频数据流 |
只记日志 |
低 |
|
|
出错 |
回传 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 ← 生命周期结束
这个设计的好处:
-
Flutter 侧的 await 一定会返回 — 无论是完成、停止还是出错,pendingResult 都会被回收
-
不会出现回调泄漏 — 每次播报都有明确的结束点
-
多次播报不会冲突 — 新的 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);
关键参数:
|
参数 |
值 |
说明 |
|---|---|---|
|
|
|
不排队——如果正在播报,新播报直接打断旧的 |
|
|
|
正常语速(1.0 倍) |
|
|
|
音量级别 |
|
|
|
正常音调 |
|
|
|
唯一标识,用于日志追踪和回调匹配 |
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 做两件事:
-
this.ttsEngine.shutdown()— 释放鸿蒙 TTS 引擎占用的音频通道和内存 -
this.ttsEngine = null— 清空引用,方便后续重新创建
shutdown 本身也包了 try-catch,防止引擎状态异常时导致插件崩溃。
在鸿蒙设备上,TTS 引擎是重资源。如果不 shutdown,引擎会一直占用音频通道,其他应用可能无法使用 TTS/ASR 功能。
关键代码位置
|
文件 |
作用 |
|---|---|
|
|
鸿蒙 TTS 插件(本文核心) |
|
|
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 数量。核心设计是:
-
pendingResult 追踪一次播报的完整生命周期 — speak 时保存,完成/停止/出错时回收
-
引擎懒加载 + 单例复用 — 首次 speak 时创建,后续复用
-
5 个监听器各有分工 — onComplete/onStop/onError 都要回收 pendingResult
-
stop 是同步返回的 — 不需要 Flutter 侧 await 等待
-
shutdownEngine 在 onDetachedFromEngine 时调用 — 确保引擎资源释放
这种"命令型能力"的插件结构很稳定,可以复用到其他鸿蒙原生能力的接入中。
更多推荐


所有评论(0)