为什么 `audioPlayerDidFinishPlaying` 在关闭它所属的当前 viewController 后仍然存在

Why `audioPlayerDidFinishPlaying` is still alive after dismiss the current viewController it belongs to

我有一个名为 PlayerViewController 的 modalPresentedVC 显示音乐播放面板,其中包括一些播放、暂停按钮和图像封面。

我实现了audioPlayerDidFinishPlaying让它在当前歌曲完成后继续播放下一首歌曲。我注意到在我关闭此 PlayerViewController viewController 后,音频播放将在当前完成后转到下一首歌曲。这就是我想要的,很酷!

但是我不明白这背后的事情。我想我已经驳回了这个 ViewController,那么无论如何都不应该从外部访问函数 audioPlayerDidFinishPlaying。但它仍然被调用,因为我把 print("I'm alive") 放在证明它的那个方法中。

那么这是否意味着 audioPlayerDidFinishPlaying 可以在其当前 viewController 之外作为内置支持工作?而且我们不需要任何额外的努力。

func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
    if flag {
        goNextTrack()
        print("I'm alive")
        // omitted code
        updateTimeLabel()
    } else {
        fatalError("the audio doesn't finished playing successfully.")
    }
}

func goNextTrack() {
    if modelController.playMode == .cycle {
        self.position = (position + 1) % songs.count
        playSong()
    } else if modelController.playMode == .cycleOne {
        playSong()
    } else {
        songs.shuffle()
        playSong()
    }
}

func playSong() {
    let song = songs[position]
    guard let url = song.path else { return }
    AudioPlayer.shared.play(url: url)
    guard let player = AudioPlayer.shared.player else { return }

    // set player.delegate to current VC
    player.delegate = self 
    // omitted code
}

playSong 中,您将 self 作为 delegate 分配给 AudioPlayer,因此它保留了对您的 PlayerViewController 的引用,因此即使您有关闭它,AudioPlayer 让它保持活动状态,并在播放完歌曲后调用 audioPlayerDidFinishPlaying

当然,下一个自然的问题是,“为什么 AudioPlayer 还活着?”我很确定那是因为它在幕后完成了在并发任务的 @escaping 闭包中播放歌曲的工作。为了完成它的工作,闭包必须捕获对播放器的引用,使其保持活动状态。

关于获取重复的 PlayerViewController 实例,那是因为 AudioPlayer 是单例并且 PlayerViewController 被用作 AudioPlayer 的委托,所以 PlayerViewControllerAudioPlayer 保持存活。再次打开视图会产生一个新的 PlayerViewController,但旧的仍然存在于 AudioPlayer

解决方案是重构 PlayerViewController 不再是 AVAudioPlayerDelegate

因此,您将 AVAudioPlayerDelegate 一致性的方法从 PlayerViewController 移至单独的对象。在下面的代码中,我假设 songsSong 的数组,并且 modelControllerModelController,因为这些定义不在提供的代码中。

在这种情况下,我认为 AVAudioPlayerDelegate 的最佳人选似乎是 AudioPlayer 本身。

此外,由于我们试图将 AudioPlayerPlayerViewController 分离,我们仍然需要一种方法让 AudioPlayer 在歌曲结束时进行通信,以便它可以更新这是时间标签。为此,我声明了一个 PlayerListener 协议(在这种情况下,“听众”指的是设计模式而不是音乐的听众)。

protocol PlayerListener: class
{
    func receivePlayerMessage(_ message: AudioPlayer.Message)
}

@objc class AudioPlayer: NSObject, AVAudioPlayerDelegate
{
    // Singleton pattern
    static let shared = AudioPlayer()
    var player: AVAudioPlayer? = nil

    var songs: [Song] = []
    var position: Int = 0
    var modelController: ModelController? = nil
    
    enum Message
    {
        case songFinished
        case songStarted
    }
    
    private var listeners: [PlayerListener] = []
    
    private override init() {}
    
    func add(listener: PlayerListener) {
        listeners.append(listener)
    }
    
    func remove(listener: PlayerListener) {
        listeners.removeAll { [=10=] === listener }
    }
    
    func broadcast(message: Message) {
        listeners.forEach { [=10=].receivePlayerMessage(message) }
    }
    
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool)
    {
        guard flag else {
            fatalError("the audio doesn't finished playing successfully.")
        }
        
        goNextTrack()
        print("I'm alive")
        broadcast(message: .songFinished)
    }
    
    func goNextTrack()
    {
        guard let playMode = modelController?.playMode else { return }
        
        switch playMode
        {
            case .cycle: position = (position + 1) % songs.count
            case .cycleOne:  break
            default: songs.shuffle()
        }
        
        playSong()
    }
    
    func playSong()
    {
        guard songs.indices.contains(position) else { return }
        
        let song = songs[position]
        guard let url = song.path else { return }
        
        play(url: url)
    }
    
    func play(url: URL)
    {
        do
        {
            player = try AVAudioPlayer(contentsOf: url)
            guard let player = player else { return }
            player.delegate = self
            player.prepareToPlay()
            player.play()
            broadcast(message: .songStarted)
        }
        catch { print(error) }
    }
}

从您的 PlayerViewController 中删除 AVAudioPlayerDelegate 一致性,并添加对 PlayerListener.

的一致性

大概它仍然需要响应 playSong() 并且可能 goNextTrack() 响应 UI 元素。此外,由于 AudioPlayer 需要歌曲数据,因此您需要在每次查看 loads/appears 时设置它。因为你提到重新使用视图控制器,让我们在 viewDidAppear() 中这样做,尽管我质疑这种方法 - 在我看来每个视图(或者可能是视图层次结构)都应该有自己的视图控制器实例。但这就是我。

// in PlayerViewController
    var position: Int
    {
        get { AudioPlayer.shared.position }
        set { AudioPlayer.shared.position = newValue }
    }
    
    func receivePlayerMessage(_ message: AudioPlayer.Message)
    {
        switch message
        {
            case .songStarted: updateTimeLabel()
            default: break
        }
    }

    override func viewDidAppear()
    {
        AudioPlayer.shared.songs = songs
        AudioPlayer.shared.modelController = modelController
        AudioPlayer.shared.add(listener: self)
    }
    
    override func viewDidDisappear() {
        AudioPlayer.shared.remove(listener: self)
    }

    func goNextTrack() {
        AudioPlayer.shared.goNextTrack()
    }

    func playSong() {
        AudioPlayer.shared.playSong()
    }

现在您在 AudioPlayer 的生命周期内拥有一名球员代表。当您的视图控制器被关闭时,它不会保持活动状态(至少不会被 AudioPlayer 保持活动状态,因此可以取消初始化和解除分配,因此您不会得到视图控制器的多个实例。

您确实提到您想要重新使用视图控制器,这就是我们在 viewDidAppear 中设置 AudioPlayer 数据而不是 viewDidLoad 的原因。我不确定这是个好主意,这是一个单独的问题。我会在评论中稍微说明一下。

更新我的代码,只为音频播放器提供一次委托。并添加 AudioPlayer API 代码。

好吧,我在这里真正挣扎的是 PlayerViewController 是一个 modalPresentedVC,它可以从不同的来源打开。在我的例子中,它可以从 tableView 行点击或 miniPlayer 容器(它是 tabBar 顶部的子视图控制器,也可以在整个应用程序页面上看到)打开。

但是因为有PlayerViewController的retain reference,所以即使是dismissing,它仍然活着。那么我怎么能从 miniPlayer 容器中打开这个相同的 modalPresentedVC,我的意思是当我点击这个 miniPlayer 容器时,这个相同的 PlayerViewController 应该再次出现在前面。

我尝试使用下面的代码再次打开这个 VC,但似乎我重新创建了一个 viewController 实例,然后我现在得到两个 viewController 实例。我怎样才能访问现有的?

let storyboard = UIStoryboard(name: "Main", bundle: nil)
guard let vc = storyboard.instantiateViewController(identifier: "Player") as? PlayerViewController else { return }
present(vc, animated: true)
func playSong() {
    let song = songs[position]
    guard let url = song.path else { return }
    AudioPlayer.shared.play(url: url)
    guard let player = AudioPlayer.shared.player else { return }

    // set player.delegate to current VC only once
    if player.delegate == nil {
        player.delegate = self
    }
    // omitted code
}

我的音频播放器API

class AudioPlayer {
    // Singleton pattern
    static let shared = AudioPlayer()
    var player: AVAudioPlayer?
    
    private init() {}
    
    func play(url: URL) {
        do {
            player = try AVAudioPlayer(contentsOf: url)
            guard let player = player else { return }
            player.prepareToPlay()
            player.play()
        } catch {
            print(error)
        }
    }
}