根本原因是 backgroundAudioManager 为单例对象,页面卸载时未手动 off 事件监听(如 onPlay、onPause、onTimeUpdate),导致回调函数持续持有组件实例引发内存泄漏;src 更新需先 pause 再重置,onTimeUpdate 中须节流避免重操作。

uni-app 中用 uni.getBackgroundAudioManager() 实现锁屏控制中心播放,但反复操作后出现卡顿、音效消失、甚至 App 崩溃,根本原因不是音频源问题,而是后台音频实例未及时清理 + 事件监听未解绑导致的内存泄漏。
为什么 backgroundAudioManager 会内存泄漏
和 uni.createInnerAudioContext() 不同,uni.getBackgroundAudioManager() 返回的是单例对象,全局唯一,但它的事件监听(如 onPlay、onTimeUpdate)如果在页面卸载时没手动 off,回调函数就会持续持有页面上下文(比如 Vue 组件实例),GC 无法回收 —— 尤其在听书类长周期页面频繁进出时,泄漏速度极快。
- Android 端表现:播放几次后音效变慢、进度条跳变、最终静音
- iOS 端表现:锁屏界面封面/标题消失,
onPlay不触发,但manager.play()仍返回 success - 鸿蒙端表现:内存占用每分钟涨 5–10MB,30 分钟后应用无响应
必须显式 off 的三个关键事件
后台音频管理器的事件监听不能靠组件销毁自动清理,必须在 beforeDestroy 或 onUnload 中手动解绑。漏掉任意一个,都会造成闭包引用链不释放。
-
manager.onPlay(() => { ... })→ 对应manager.offPlay(...) -
manager.onPause(() => { ... })→ 对应manager.offPause(...) -
manager.onTimeUpdate(() => { ... })→ 对应manager.offTimeUpdate(...)
注意:onEnded 和 onError 也建议统一 off,虽然它们触发频率低,但一旦监听函数里用了 this 或局部变量,同样会拖住内存。
src 更新时别直接赋值,先 pause 再 reset
切换音频时若直接改 manager.src,部分 Android 机型(尤其 OPPO、vivo 定制 ROM)会残留旧音频缓冲,导致内存持续增长。正确做法是先暂停、清空状态、再设新 src:
const manager = uni.getBackgroundAudioManager() manager.pause() // 清除可能残留的 timeupdate 监听(尤其之前没 off 干净) manager.offTimeUpdate() manager.src = '/static/book/chapter2.mp3' manager.title = '第二章' manager.play()
不要省略 pause() —— 否则新 play() 可能与旧播放线程冲突,触发底层资源重复申请。
避免在 onTimeUpdate 里做重操作
onTimeUpdate 在 iOS 和鸿蒙上默认每 250ms 触发一次,Android 部分机型甚至更密。如果在里面执行 this.$forceUpdate()、复杂计算或调用 uni.setStorageSync,会快速堆积 JS 调用栈,加剧内存压力。
- 只更新必要字段:如
this.currentTime = manager.currentTime - 用
throttle限制 UI 更新频率(例如 500ms 一次) - 绝对不要在其中 new Audio()、创建 canvas、解析 base64 图片
真机测试发现,未节流的 onTimeUpdate 持续运行 10 分钟,iOS 内存上涨超 80MB,且 GC 无法及时回收。
最易被忽略的是:即使你只用一个 manager 实例,只要事件监听没 clean,每次页面进入都叠加一层闭包引用 —— 三天连续使用后,单个页面实例可能被 20+ 个未解绑的 onTimeUpdate 持有,内存就卡死了。


















