由于 AVPlayer 和观察者的初始化失败导致内存泄漏

Memory leak due to failed deinitialization of AVPlayer and observers

在 10/2020 中编辑:以前的标题是“未在 UIPageViewController 的 UIViewController 中调用 Deinitializer”

我希望我的 UIViewController(s)(它是 UIPageViewController 的一部分)中的以下去初始化器删除我的 playerLayer 并将 player 设置为 nil 以便内存没有超载(因为当 UIViewController 不再需要时应该总是调用 deinit):

deinit {
        
    self.player = nil
    self.playerLayer.removeFromSuperlayer()
    print("deinit")
    
}

为了检查 deinit 是否曾经执行过,我添加了打印,发现它从未被调用过。有人可以解释为什么不叫它吗?你会建议我做什么来实现我想做的事?

编辑:

按照 Rob(在评论中)建议的 ,我发现以下函数会导致内存泄漏。如果可以在文档目录中找到文件,该函数应该设置播放器。

setupPlayer() 函数:

//setup video player
func setupPlayer() {
    
    //get name of file on server //self.video is a String containing the URL for a video on a server
    let fileName = URL(string: self.video!)!.lastPathComponent
    
    let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
    let url = NSURL(fileURLWithPath: path)
    let filePath = url.appendingPathComponent(fileName)?.path
    let fileManager = FileManager.default
    
    if fileManager.fileExists(atPath: filePath!) {
    
        //create file with name on server if not there already
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
        if let docDir = paths.first
        {
            
            let appFile  = docDir.appending("/" + fileName)
            let videoFileUrl = URL(fileURLWithPath: appFile)
            
            //player's video
            if self.player == nil {
                
                let playerItemToBePlayed = AVPlayerItem(url: videoFileUrl) //AVPlayerItem(url: videoFileUrl)
                
                self.player = AVPlayer(playerItem: playerItemToBePlayed)
                
                //add sub-layer
                playerLayer = AVPlayerLayer(player: self.player)
                playerLayer.frame = self.view.frame
                self.controlsContainerView.layer.insertSublayer(playerLayer, at: 0)
                
                //when are frames actually rendered (when is video loaded)
                self.player?.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context:nil)
                
                //loop through video
                NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.player?.currentItem, queue: nil, using: { (_) in
                    DispatchQueue.main.async {
                        self.player?.seek(to: kCMTimeZero)
                        self.player?.play()
                    }
                })
                
            }
            
        }
        
    }

}

pageViewController函数(viewcontrollerAfter)

func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? 
{
    
    let currentIndexString  = (viewController as! MyViewController).index
    let currentIndex        = indec.index(of: currentIndexString!)
    
    //set if so that next page
    if currentIndex! < indec.count - 1 {
    
        //template
        let myViewController = MyViewController()
    
        //enter data into template
        myViewController.index            = self.indec[currentIndex! + 1]
    
        //return template with data
        return myViewController
        
    }
    
    return nil
    
}

编辑 2:

如您所见,没有回溯,请注意此 malloc(右上)和类似大 malloc(左下)的大小。

如果我们查看 "Debug Memory Graph" 中的对象图,我们可以看到:

我们可以看到视图控制器被闭包(中间路径)捕获。我们还可以看到观察者保持着强烈的参考(底部路径)。

因为我打开了 "Malloc stack" 功能(显示在 中),我可以点击 "Closure Captures" 并且可以在右侧面板中看到堆栈跟踪:

(请原谅我,该内存图与第一个屏幕快照略有不同,因为我修复了另一个内存问题,观察者,如本答案末尾所述。)

无论如何,如果我单击堆栈跟踪中黑色的最高条目(即该堆栈跟踪中我自己的代码的最后一位),它会直接将我们带到有问题的代码:

请注意您的原始代码:

NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.player?.currentItem, queue: nil, using: { (_) in
    DispatchQueue.main.async {
        self.player?.seek(to: kCMTimeZero)
        self.player?.play()
    }
})

闭包保持对 self 的强引用。您可以通过以下方式更正:

NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem, queue: .main) { [weak self] _ in
    self?.player?.seek(to: kCMTimeZero)
    self?.player?.play()
}

请注意,闭包中的 [weak self] 捕获列表。


顺便说一句,虽然您不需要 nil 您的 playerdeinit 中,但您确实需要移除观察者。我还会为你的观察者设置一个 context,这样你的 observerValue(forKeyPath:of:change:context:) 就可以知道它是否需要处理。

所以这可能会导致类似的结果:

private var observerContext = 0
private weak var observer: NSObjectProtocol?

func setupPlayer() {
    let fileName = URL(string: video!)!.lastPathComponent

    let fileManager = FileManager.default
    let videoFileUrl = try! fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
        .appendingPathComponent(fileName)

    if fileManager.fileExists(atPath: videoFileUrl.path), player == nil {
        let playerItemToBePlayed = AVPlayerItem(url: videoFileUrl)

        player = AVPlayer(playerItem: playerItemToBePlayed)

        //add sub-layer
        playerLayer = AVPlayerLayer(player: player)
        playerLayer.frame = view.bounds
        controlsContainerView.layer.insertSublayer(playerLayer, at: 0)

        //when are frames actually rendered (when is video loaded)
        player?.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: &observerContext)

        //loop through video
        observer = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem, queue: .main) { [weak self] _ in
            self?.player?.seek(to: kCMTimeZero)
            self?.player?.play()
        }
    }
}

deinit {
    print("deinit")

    // remove loadedTimeRanges observer 

    player?.removeObserver(self, forKeyPath: "currentItem.loadedTimeRanges")

    // remove AVPlayerItemDidPlayToEndTime observer

    if let observer = observer {
        NotificationCenter.default.removeObserver(observer)
    }
}

// note, `observeValue` should check to see if this is something 
// this registered for or whether it should pass it along to `super`

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    guard context == &observerContext else {
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        return
    }

    // do something
}