You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何停止或覆盖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)
        }
    }
}

核心问题点

  1. clearPlayerInstance逻辑错误:直接依赖SessionManager.shared.audioPlayer导致循环引用,且未先暂停播放就清空队列。
  2. 清理不彻底:手动失效KVO时遗漏实例内的观察者属性,未处理媒体时间观察者,闭包和信号订阅未清空导致内存泄漏。
  3. 播放器实例混用:同时维护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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 15:45:43