HarmonyOS 应用开发《掌上英语》第31篇:AudioPlayer 单例封装:HarmonyOS 音频播放最佳实践
31:AudioPlayer 单例封装:HarmonyOS 音频播放最佳实践

一、引言
在 HarmonyOS 英语学习 App 中,音频播放是核心功能之一——单词发音、例句朗读、听力练习都需要稳定可靠的音频播放能力。如果每个页面各自创建和销毁播放器实例,不仅会造成资源浪费,还会出现多个音频同时播放的混乱局面。为此,我们设计了一个基于单例模式的 AudioPlayer 类,集中管理 AVPlayer 的完整生命周期。
本文将基于实际源码,深入分析单例模式在多媒体播放场景下的应用、AVPlayer 的异步初始化、状态管理以及异常处理机制。
二、单例模式的设计与实现
2.1 为什么需要单例
音频播放器属于典型的资源型对象,它持有底层的媒体解码器、音频输出设备等系统资源。如果每个页面都 new AudioPlayer(),会导致:
- 资源竞争:多个播放实例同时操作音频设备,产生杂音或冲突
- 内存泄漏:页面退出后忘记释放播放器,造成资源泄漏
- 状态混乱:无法统一获知当前是否有音频正在播放
单例模式确保全局只有一个 AudioPlayer 实例,所有页面共享同一个播放器,从根本上解决了上述问题。
2.2 代码实现
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Logger } from './Logger';
export class AudioPlayer {
private static instance?: AudioPlayer;
private avPlayer: media.AVPlayer | null = null;
private currentUrl: string = '';
private constructor() {}
private async initPlayer(): Promise<void> {
try {
this.avPlayer = await media.createAVPlayer();
this.initPlayerEvents();
} catch (e) {
Logger.error('AudioPlayer', '初始化失败');
}
}
public static getInstance(): AudioPlayer {
if (!AudioPlayer.instance) {
AudioPlayer.instance = new AudioPlayer();
AudioPlayer.instance.initPlayer();
}
return AudioPlayer.instance;
}
// ...
}
2.3 设计要点解析
私有构造函数:private constructor() 防止外部通过 new AudioPlayer() 创建实例,强制所有调用方走 getInstance() 工厂方法。
延迟初始化:AudioPlayer.instance 初始为 undefined,只有在第一次调用 getInstance() 时才创建实例并初始化 AVPlayer。这种 Lazy Initialization 模式避免了 App 启动时不必要的资源开销。
异步初始化:media.createAVPlayer() 是一个异步操作,initPlayer() 方法被设计为 async,在创建成功后挂载事件监听器。这里有一个细节:如果 initPlayer 尚未完成时第二个线程调用了 getInstance(),instance 已经不为空但 avPlayer 可能还是 null。在实际使用中,play() 方法内部会判空处理,确保安全。
三、AVPlayer 的异步创建流程
private async initPlayer(): Promise<void> {
try {
this.avPlayer = await media.createAVPlayer();
this.initPlayerEvents();
} catch (e) {
Logger.error('AudioPlayer', '初始化失败');
}
}
media.createAVPlayer() 是 HarmonyOS 提供的媒体框架 API,它返回一个 Promise<media.AVPlayer>。在等待 Promise resolve 的过程中,初始化函数不会阻塞主线程。创建成功后,紧接着调用 initPlayerEvents() 注册事件回调。
异常处理:使用 try/catch 包裹整个初始化过程。如果创建失败(例如系统媒体服务不可用),日志系统会记录错误,avPlayer 保持为 null,后续所有播放操作都会因空指针检查而安全跳过。
四、事件系统的初始化
private initPlayerEvents(): void {
if (!this.avPlayer) return;
this.avPlayer.on('error', (err: BusinessError) =>
Logger.error('AudioPlayer', `错误: ${JSON.stringify(err)}`));
this.avPlayer.on('endOfStream', () =>
Logger.info('AudioPlayer', '播放完成'));
}
事件监听是音频播放可靠性的基石。这里注册了两个最关键的事件:
-
error 事件:当播放过程中发生解码错误、网络超时或文件格式不支持等问题时触发。使用
JSON.stringify(err)将错误信息序列化输出到日志,便于排查问题。 -
endOfStream 事件:音频文件播放完毕后触发,用于通知播放完成。在更复杂的场景中,可以在这里触发播放下一个音频或更新 UI 状态。
值得注意的是,实际生产环境还可以注册 stateChange 事件来监听 AVPlayer 的状态流转,这部分将在后续文章中深入分析。
五、核心方法详解
5.1 play 方法——智能播放控制
public play(url: string): void {
if (!url || url === '') {
Logger.warn('AudioPlayer', 'URL为空');
return;
}
if (!this.avPlayer) return;
if (this.currentUrl === url && this.avPlayer.state === 'playing') {
this.stop();
return;
}
this.currentUrl = url;
this.avPlayer.stop();
let src = url.startsWith('http') ? url : `@rawfile/${url}`;
this.avPlayer.url = src;
this.avPlayer.prepare().then(() => {
if (this.avPlayer) this.avPlayer.play();
});
}
play() 方法体现了几个精心设计的逻辑:
- 防御性编程:首先检查 URL 是否为空,防止空值导致崩溃
- 重复播放检测:如果当前正在播放同一个 URL,则调用
stop()停止——这符合用户"点击同一个发音按钮再次播放"的交互预期 - 资源重置:每次播放前先
stop(),确保播放器回到可配置状态 - 远程/本地资源适配:通过
startsWith('http')判断是远程 URL 还是本地 rawfile 资源,自动拼接路径 - 异步准备:
prepare().then()在准备完成后才调用play(),符合 AVPlayer 的状态机要求
5.2 stop / pause / release 方法
public stop(): void {
if (!this.avPlayer) return;
if (this.avPlayer.state === 'playing' || this.avPlayer.state === 'paused')
this.avPlayer.stop();
}
public pause(): void {
if (!this.avPlayer) return;
if (this.avPlayer.state === 'playing') this.avPlayer.pause();
}
public release(): void {
this.stop();
if (this.avPlayer) this.avPlayer.release();
this.avPlayer = null;
AudioPlayer.instance = undefined;
}
每个方法都遵循相同的模式:空值检查 → 状态判断 → 执行操作。stop() 和 pause() 的区别在于:
stop():停止播放,释放解码资源,播放器回到prepared或initial状态。再次播放需要重新设置 URL 并prepare()pause():暂停播放,保留解码状态。调用play()可直接恢复
release() 方法用于彻底销毁播放器,在所有操作后将 instance 置为 undefined,使单例回归初始状态,这在应用退出或需要重置音频系统时非常关键。
六、状态管理与安全边界
AudioPlayer 内部维护了 currentUrl 状态变量,用于追踪当前加载的音频资源。结合 AVPlayer 自身的状态机,形成了双层状态管理:
| 场景 | currentUrl | avPlayer.state | 行为 |
|---|---|---|---|
| 首次播放 | 设为新URL | idle → prepared → playing | 正常播放流程 |
| 重复点击 | 与传入URL相同 | playing | 停止播放 |
| 切换音频 | 更新为新URL | 先stop,再prepare→play | 无缝切换 |
| URL为空 | 不变 | 不变 | 直接返回 |
七、异常处理最佳实践
在整个 AudioPlayer 中,异常处理遵循分层策略:
第一层:参数校验——play() 开头检查 URL 是否为空,用 Logger.warn 记录警告而非 Error,因为空 URL 属于预期内的调用方失误。
第二层:状态校验——每个操作前检查 this.avPlayer 是否为 null,防止在初始化失败或已释放后继续操作。
第三层:异步异常——media.createAVPlayer() 的 try/catch 捕获创建失败;prepare().then() 中理论上还应链式 .catch() 处理准备失败的情况(当前源码中未包含,生产环境建议补全)。
八、总结
AudioPlayer 的单例封装设计,在代码层面实现了三个核心目标:
- 资源复用:全局共享一个 AVPlayer 实例,避免重复创建和销毁
- 接口简洁:对外暴露
play/stop/pause/release四个方法,调用方无需关心底层状态机 - 安全可靠:防御性编程 + 分层异常处理,确保任何异常场景都不会导致应用崩溃
这套设计模式不仅适用于音频播放,对于相机预览、视频播放、录音等所有需要独占系统资源的场景,都具有普遍的参考价值。
更多推荐


所有评论(0)