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

Posted

技术标签:

【中文标题】由于 AVPlayer 和观察者的反初始化失败导致的内存泄漏【英文标题】:Memory leak due to failed deinitialization of AVPlayer and observers 【发布时间】:2017-06-17 18:08:14 【问题描述】:

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

我希望我的 UIViewController(s)(它是 UIPageViewController 的一部分)中的以下反初始化程序删除我的 playerLayer 并将 player 设置为 nil 以便内存不会过载(因为 @987654328当 UIViewController 可以说不再需要时,应该总是调用 @):

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

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

编辑:

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

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(左下角)的大小。

【问题讨论】:

首先,您不必在deinit 中设置nil 属性。根据定义,如果调用了deinit,那么无论如何这些引用都将被释放。其次,查看控制器没有被释放,我们必须看看你的页面视图控制器是如何管理它的子控制器的。我敢打赌你会在那里保留一些参考资料。或者某处有一些强大的参考周期。仅供参考,您可以运行应用程序,然后使用“调试内存图”功能来查看是什么保持了对视图控制器的强引用。见***.com/a/30993476/1271826 @Rob 我找到了导致内存泄漏的函数并将其编辑到我的答案中。可以假设是玩家。此外,我在我的问题中编辑了一个 pageviewcontroller 函数(之后的 viewcontroller),因为我无法真正看到我在哪里留下了参考资料……如果你能看一下,我将不胜感激! 与其倾注代码并试图猜测是什么保持对已解散视图控制器的强引用,我会在调试器中运行应用程序,做任何你希望它被释放的事情,然后使用 Xcode 的“调试内存图”功能来精确地找出是什么保持了对应该被释放的视图控制器的强引用。一旦你看到是什么保留了那个强引用,解决这个引用就会容易得多。也许与我们分享内存图图像。 我试图完全追溯它,但由于没有提供追溯(s.“EDIT 2”)而无法这样做。我唯一能发现的就是它一定是函数。 使用内存图时,我不会从 malloc 块开始(因为这些通常在框架内部完成,很难与我们的代码相关联......而且可能有一些我们无法控制的微不足道的泄漏或误报)。首先关注你的对象。例如,在下面,我专注于我没有看到 deinit 的对象(即视图控制器),这使问题变得明显。 【参考方案1】:

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

我们可以看到视图控制器被闭包(中间路径)捕获。我们还可以看到观察者正在保持强引用(底部路径)。

因为我打开了“Malloc stack”功能(显示在https://***.com/a/30993476/1271826),所以我可以点击“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] 捕获列表。


顺便说一句,虽然您不需要在deinit 中将nilplayer 联系起来,但您确实需要移除观察者。我还会为您的观察者设置一个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

【讨论】:

不幸的是,这会导致应用程序崩溃。我真的相信这是功能...... s。编辑 2 是的,我也看到了崩溃,这是一个单独的问题,即无法删除观察者。请参阅修改后的答案。 我会再次检查编辑。 (第一条评论与第一个版本有关) 这完美!非常感谢您的奉献和帮助!非常感谢!

以上是关于由于 AVPlayer 和观察者的反初始化失败导致的内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

由于间隙,AVPlayer 流在后台停止 [关闭]

Swift 编译器错误,由于信号导致命令失败:分段错误:11

终止应用程序原因:“AVPlayer 的实例无法删除由 AVPlayer 的不同实例添加的时间观察者。”

AVPlayer 项目缓冲区为空

为啥 AVPlayer 边界时间观察器不起作用?

AVPlayer 暂停后无法继续播放