由于 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
中将nil
与player
联系起来,但您确实需要移除观察者。我还会为您的观察者设置一个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 和观察者的反初始化失败导致的内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章
Swift 编译器错误,由于信号导致命令失败:分段错误:11