如何停止或覆盖AVPlayer实例?解决多音频同时播放问题
解决AVQueuePlayer多实例同时播放导致双重音频的问题
我为各类音乐列表实现了通用的AudioPlayer组件,但切换播放新曲目时,旧的播放器实例没有被正确停止或销毁,导致两个音频同时播放。尝试过暂停播放器、将AudioPlayer置为nil,但问题依然存在。
问题代码分析
SessionManager 单例
class SessionManager { static let shared = SessionManager() var isPlayingMusic: Bool = false var isAdFree: Bool = false var audioPlayer: AudioPlayer? var updateShuffleBtnUI: (() -> Void)? var didShuffleList: Bool = false var isShuffleOn: Bool = false { didSet { updateShuffleBtnUI?() } } func setTheme() { UIView.transition(with: appDelegate?.window ?? UIWindow(), duration: 0.5, options: [.preferredFramesPerSecond60]) { appDelegate?.window?.overrideUserInterfaceStyle = self.isDarkTheme ? .dark : .light } } func clear() { isPlayingMusic = false audioPlayer?.clearPlayerInstance() } }
AudioPlayer 类(精简版)
注:已移除部分方法和对象
import Foundation import AVFoundation import UIKit import MediaPlayer class AudioPlayer: NSObject { // 各类KVO观察者 var audioQueueObserver: NSKeyValueObservation? var audioQueueStatusObserver: NSKeyValueObservation? var audioQueueBufferEmptyObserver: NSKeyValueObservation? var audioQueueBufferAlmostThereObserver: NSKeyValueObservation? var audioQueueBufferFullObserver: NSKeyValueObservation? var audioQueueStallObserver: NSKeyValueObservation? var audioQueueWaitingObserver: NSKeyValueObservation? var assetPoolObserver: NSKeyValueObservation? var playbackLikelyToKeepUpKeyPathObserver: NSKeyValueObservation? var playbackBufferEmptyObserver: NSKeyValueObservation? var playbackBufferFullObserver: NSKeyValueObservation? // 信号与闭包 let onError = Signal<(message: String, error: Error)>() let onCollectionReady = Signal<Bool>() var updatePlayPauseStatus: ((Bool) -> Void)? var updateSliderAndTime: ((_ listenedTime: String, _ sliderProgress: Float, _ passTimeStr: String?, _ totalDuration: Float?) -> Void)? // 播放器实例 var player = AVPlayer() var playerQueue: AVQueuePlayer? private var mediaTimeObserver: Any? // 清理方法(原逻辑存在问题) func clearPlayerInstance() { SessionManager.shared.audioPlayer?.playerQueue?.removeAllItems() MPNowPlayingInfoCenter.default().nowPlayingInfo = nil SessionManager.shared.audioPlayer = nil } deinit { /// Remove any KVO observer. self.audioQueueObserver?.invalidate() self.audioQueueStatusObserver?.invalidate() self.audioQueueBufferEmptyObserver?.invalidate() self.audioQueueBufferAlmostThereObserver?.invalidate() self.audioQueueBufferFullObserver?.invalidate() var audioQueueStallObserver: NSKeyValueObservation? var audioQueueWaitingObserver: NSKeyValueObservation? var mediaTimeObserver: Any? self.onCollectionReady.cancelAllSubscriptions() } }
跳转播放曲目代码
func openMusicPlayer(type: PlayerType, songObj: SongModel? = nil, musicUrlList: [SongModel]? = nil) { let musicPlayer = Router.musicPlayer.instance as? MusicPlayerVC ?? MusicPlayerVC() musicPlayer.type = type musicPlayer.songObj = songObj musicPlayer.musicUrlList = musicUrlList musicPlayer.hidesBottomBarWhenPushed = true if SessionManager.shared.isPlayingMusic { SessionManager.shared.audioPlayer?.audioQueueObserver?.invalidate() SessionManager.shared.audioPlayer?.audioQueueStatusObserver?.invalidate() SessionManager.shared.audioPlayer?.audioQueueBufferEmptyObserver?.invalidate() SessionManager.shared.audioPlayer?.audioQueueBufferAlmostThereObserver?.invalidate() SessionManager.shared.audioPlayer?.audioQueueBufferFullObserver?.invalidate() var audioQueueStallObserver: NSKeyValueObservation? var audioQueueWaitingObserver: NSKeyValueObservation? var mediaTimeObserver: Any? SessionManager.shared.audioPlayer?.onCollectionReady.cancelAllSubscriptions() SessionManager.shared.isPlayingMusic = false SessionManager.shared.audioPlayer?.pause() SessionManager.shared.audioPlayer?.player.replaceCurrentItem(with: nil) SessionManager.shared.audioPlayer = nil } if type == .audio { navigationController?.pushViewController(musicPlayer, animated: true) } else { showLoginRequiredAlert { [weak self] in guard let self = self else { return } self.navigationController?.pushViewController(musicPlayer, animated: true) } } }
核心问题点
clearPlayerInstance逻辑错误:直接依赖SessionManager.shared.audioPlayer导致循环引用,且未先暂停播放就清空队列。- 清理不彻底:手动失效KVO时遗漏实例内的观察者属性,未处理媒体时间观察者,闭包和信号订阅未清空导致内存泄漏。
- 播放器实例混用:同时维护
player和playerQueue,清理时只处理了player的内容,忽略了实际播放用的playerQueue。
解决方案
1. 修复clearPlayerInstance方法
改为实例内自清理逻辑,不依赖外部单例:
func clearPlayerInstance() { // 暂停播放 playerQueue?.pause() // 清空队列所有项目 playerQueue?.removeAllItems() // 失效所有KVO观察者 audioQueueObserver?.invalidate() audioQueueStatusObserver?.invalidate() audioQueueBufferEmptyObserver?.invalidate() audioQueueBufferAlmostThereObserver?.invalidate() audioQueueBufferFullObserver?.invalidate() audioQueueStallObserver?.invalidate() audioQueueWaitingObserver?.invalidate() assetPoolObserver?.invalidate() playbackLikelyToKeepUpKeyPathObserver?.invalidate() playbackBufferEmptyObserver?.invalidate() playbackBufferFullObserver?.invalidate() // 移除媒体时间观察者 if let observer = mediaTimeObserver { playerQueue?.removeTimeObserver(observer) mediaTimeObserver = nil } // 清空通知栏信息 MPNowPlayingInfoCenter.default().nowPlayingInfo = nil // 取消所有信号订阅 onCollectionReady.cancelAllSubscriptions() onError.cancelAllSubscriptions() // 清空闭包引用,避免内存泄漏 updatePlayPauseStatus = nil updateSliderAndTime = nil updateRepeatBtnUI = nil loadingStatus = nil loadBottomBarViewInTab = nil firstAssetIsLoaded = nil songDetailClosure = nil mediaItemChange = nil shuffleCompletionClosure = nil }
2. 优化SessionManager的clear方法
func clear() { isPlayingMusic = false audioPlayer?.clearPlayerInstance() audioPlayer = nil }
3. 简化跳转播放时的清理逻辑
func openMusicPlayer(type: PlayerType, songObj: SongModel? = nil, musicUrlList: [SongModel]? = nil) { let musicPlayer = Router.musicPlayer.instance as? MusicPlayerVC ?? MusicPlayerVC() musicPlayer.type = type musicPlayer.songObj = songObj musicPlayer.musicUrlList = musicUrlList musicPlayer.hidesBottomBarWhenPushed = true if SessionManager.shared.isPlayingMusic { // 调用统一清理方法 SessionManager.shared.clear() } if type == .audio { navigationController?.pushViewController(musicPlayer, animated: true) } else { showLoginRequiredAlert { [weak self] in guard let self = self else { return } self.navigationController?.pushViewController(musicPlayer, animated: true) } } }
4. 完善deinit兜底逻辑
deinit { clearPlayerInstance() print("AudioPlayer instance deinitialized") // 用于验证实例是否被正确释放 }
额外检查
- 确保
MusicPlayerVC销毁时调用SessionManager.shared.clear(),避免控制器持有播放器引用导致泄漏。 - 所有闭包使用
[weak self]捕获,防止循环引用。
内容的提问来源于stack exchange,提问作者steveSarsawa
相关产品推荐
相关产品推荐

