31:AudioPlayer 单例封装:HarmonyOS 音频播放最佳实践

在这里插入图片描述

一、引言

在 HarmonyOS 英语学习 App 中,音频播放是核心功能之一——单词发音、例句朗读、听力练习都需要稳定可靠的音频播放能力。如果每个页面各自创建和销毁播放器实例,不仅会造成资源浪费,还会出现多个音频同时播放的混乱局面。为此,我们设计了一个基于单例模式AudioPlayer 类,集中管理 AVPlayer 的完整生命周期。

本文将基于实际源码,深入分析单例模式在多媒体播放场景下的应用、AVPlayer 的异步初始化、状态管理以及异常处理机制。

二、单例模式的设计与实现

2.1 为什么需要单例

音频播放器属于典型的资源型对象,它持有底层的媒体解码器、音频输出设备等系统资源。如果每个页面都 new AudioPlayer(),会导致:

  1. 资源竞争:多个播放实例同时操作音频设备,产生杂音或冲突
  2. 内存泄漏:页面退出后忘记释放播放器,造成资源泄漏
  3. 状态混乱:无法统一获知当前是否有音频正在播放

单例模式确保全局只有一个 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() 方法体现了几个精心设计的逻辑:

  1. 防御性编程:首先检查 URL 是否为空,防止空值导致崩溃
  2. 重复播放检测:如果当前正在播放同一个 URL,则调用 stop() 停止——这符合用户"点击同一个发音按钮再次播放"的交互预期
  3. 资源重置:每次播放前先 stop(),确保播放器回到可配置状态
  4. 远程/本地资源适配:通过 startsWith('http') 判断是远程 URL 还是本地 rawfile 资源,自动拼接路径
  5. 异步准备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():停止播放,释放解码资源,播放器回到 preparedinitial 状态。再次播放需要重新设置 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 的单例封装设计,在代码层面实现了三个核心目标:

  1. 资源复用:全局共享一个 AVPlayer 实例,避免重复创建和销毁
  2. 接口简洁:对外暴露 play/stop/pause/release 四个方法,调用方无需关心底层状态机
  3. 安全可靠:防御性编程 + 分层异常处理,确保任何异常场景都不会导致应用崩溃

这套设计模式不仅适用于音频播放,对于相机预览、视频播放、录音等所有需要独占系统资源的场景,都具有普遍的参考价值。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐