由于 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
您的 player
在 deinit
中,但您确实需要移除观察者。我还会为你的观察者设置一个 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
}
在 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" 功能(显示在
(请原谅我,该内存图与第一个屏幕快照略有不同,因为我修复了另一个内存问题,观察者,如本答案末尾所述。)
无论如何,如果我单击堆栈跟踪中黑色的最高条目(即该堆栈跟踪中我自己的代码的最后一位),它会直接将我们带到有问题的代码:
请注意您的原始代码:
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
您的 player
在 deinit
中,但您确实需要移除观察者。我还会为你的观察者设置一个 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
}