适合谁看想读懂TextToSpeechPlugin.ets的开发者想写命令型鸿蒙原生插件的人想理解 TTS 引擎生命周期管理的人问题背景TTS 插件常见的问题不是能不能播报而是引擎是否重复创建完成和停止如何区分页面多次点击播报按钮时状态怎么收口pendingResult 挂起后怎么回收这些都决定了插件是否可维护。项目中的真实场景食界探味的 TTS 插件位于app/ohos/entry/src/main/ets/plugins/TextToSpeechPlugin.etsFlutter 侧对应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 和 detachonAttachedToEngine(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 引擎 }onAttachedToEngineFlutter 引擎启动时调用创建 MethodChannel 并注册处理器。onDetachedFromEngineFlutter 引擎销毁时调用做两件事清空 MethodChannel 处理器防止野指针调用释放 TTS 引擎归还音频资源这两个方法是鸿蒙 Flutter 插件的标准生命周期必须正确实现。三、方法分发——onMethodCallonMethodCall(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): Promisevoid { // 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(): Promisevoid { 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串行调用。引擎参数说明参数值为什么选这个值languagezh-CN中文用户person0默认发音人online1在线模式音质更好styleinteraction-broadcast广播风格适合推荐场景locateCN中国区服务器六、监听器——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 ← 生命周期结束这个设计的好处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 的区别维度handleSpeakhandleStop返回时机播报完成后才返回立即返回pendingResult需要等监听器回收直接 result.success()异步性async同步stop 是同步返回的——调用后立即告诉 Flutter停止指令已发送。实际的停止效果由鸿蒙 TTS 引擎异步处理停止完成后触发onStop回调。这意味着 Flutter 侧调用stop()后不需要await等待停止完成可以立即更新 UI。九、播报参数——setupListenerAndSpeak() 的后半段const extraParam: Recordstring, 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);关键参数参数值说明queueMode0不排队——如果正在播报新播报直接打断旧的speed1正常语速1.0 倍volume2音量级别pitch1正常音调requestIdtts_时间戳唯一标识用于日志追踪和回调匹配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 功能。关键代码位置文件作用app/ohos/entry/src/main/ets/plugins/TextToSpeechPlugin.ets鸿蒙 TTS 插件本文核心app/lib/core/platform/text_to_speech_channel.dartFlutter 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): Promisevoid { 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(): Promisevoid { 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 都要回收 pendingResultstop 是同步返回的— 不需要 Flutter 侧 await 等待shutdownEngine 在 onDetachedFromEngine 时调用— 确保引擎资源释放这种命令型能力的插件结构很稳定可以复用到其他鸿蒙原生能力的接入中。
解析鸿蒙 TextToSpeechPlugin:引擎创建、监听器和 stop 控制
适合谁看想读懂TextToSpeechPlugin.ets的开发者想写命令型鸿蒙原生插件的人想理解 TTS 引擎生命周期管理的人问题背景TTS 插件常见的问题不是能不能播报而是引擎是否重复创建完成和停止如何区分页面多次点击播报按钮时状态怎么收口pendingResult 挂起后怎么回收这些都决定了插件是否可维护。项目中的真实场景食界探味的 TTS 插件位于app/ohos/entry/src/main/ets/plugins/TextToSpeechPlugin.etsFlutter 侧对应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 和 detachonAttachedToEngine(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 引擎 }onAttachedToEngineFlutter 引擎启动时调用创建 MethodChannel 并注册处理器。onDetachedFromEngineFlutter 引擎销毁时调用做两件事清空 MethodChannel 处理器防止野指针调用释放 TTS 引擎归还音频资源这两个方法是鸿蒙 Flutter 插件的标准生命周期必须正确实现。三、方法分发——onMethodCallonMethodCall(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): Promisevoid { // 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(): Promisevoid { 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串行调用。引擎参数说明参数值为什么选这个值languagezh-CN中文用户person0默认发音人online1在线模式音质更好styleinteraction-broadcast广播风格适合推荐场景locateCN中国区服务器六、监听器——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 ← 生命周期结束这个设计的好处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 的区别维度handleSpeakhandleStop返回时机播报完成后才返回立即返回pendingResult需要等监听器回收直接 result.success()异步性async同步stop 是同步返回的——调用后立即告诉 Flutter停止指令已发送。实际的停止效果由鸿蒙 TTS 引擎异步处理停止完成后触发onStop回调。这意味着 Flutter 侧调用stop()后不需要await等待停止完成可以立即更新 UI。九、播报参数——setupListenerAndSpeak() 的后半段const extraParam: Recordstring, 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);关键参数参数值说明queueMode0不排队——如果正在播报新播报直接打断旧的speed1正常语速1.0 倍volume2音量级别pitch1正常音调requestIdtts_时间戳唯一标识用于日志追踪和回调匹配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 功能。关键代码位置文件作用app/ohos/entry/src/main/ets/plugins/TextToSpeechPlugin.ets鸿蒙 TTS 插件本文核心app/lib/core/platform/text_to_speech_channel.dartFlutter 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): Promisevoid { 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(): Promisevoid { 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 都要回收 pendingResultstop 是同步返回的— 不需要 Flutter 侧 await 等待shutdownEngine 在 onDetachedFromEngine 时调用— 确保引擎资源释放这种命令型能力的插件结构很稳定可以复用到其他鸿蒙原生能力的接入中。