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';exportclassAudioPlayer{privatestaticinstance?:AudioPlayer;privateavPlayer:media.AVPlayer|null=null;privatecurrentUrl:string='';privateconstructor(){}privateasyncinitPlayer():Promise<void>{try{this.avPlayer=awaitmedia.createAVPlayer();this.initPlayerEvents();}catch(e){Logger.error('AudioPlayer','初始化失败');}}publicstaticgetInstance():AudioPlayer{if(!AudioPlayer.instance){AudioPlayer.instance=newAudioPlayer();AudioPlayer.instance.initPlayer();}returnAudioPlayer.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 的异步创建流程
privateasyncinitPlayer():Promise<void>{try{this.avPlayer=awaitmedia.createAVPlayer();this.initPlayerEvents();}catch(e){Logger.error('AudioPlayer','初始化失败');}}media.createAVPlayer()是 HarmonyOS 提供的媒体框架 API,它返回一个Promise<media.AVPlayer>。在等待 Promise resolve 的过程中,初始化函数不会阻塞主线程。创建成功后,紧接着调用initPlayerEvents()注册事件回调。
异常处理:使用try/catch包裹整个初始化过程。如果创建失败(例如系统媒体服务不可用),日志系统会记录错误,avPlayer保持为null,后续所有播放操作都会因空指针检查而安全跳过。
四、事件系统的初始化
privateinitPlayerEvents():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 方法——智能播放控制
publicplay(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();letsrc=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 方法
publicstop():void{if(!this.avPlayer)return;if(this.avPlayer.state==='playing'||this.avPlayer.state==='paused')this.avPlayer.stop();}publicpause():void{if(!this.avPlayer)return;if(this.avPlayer.state==='playing')this.avPlayer.pause();}publicrelease():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四个方法,调用方无需关心底层状态机 - 安全可靠:防御性编程 + 分层异常处理,确保任何异常场景都不会导致应用崩溃
这套设计模式不仅适用于音频播放,对于相机预览、视频播放、录音等所有需要独占系统资源的场景,都具有普遍的参考价值。