SpriteKit 没有释放所有已用内存

2023-11-24

我已经在 SO 和其他网站上准备了许多(如果不是全部)关于处理 SpriteKit 和内存问题的灾难的文章。正如许多其他人所遇到的那样,我的问题是在我离开 SpriteKit 场景后,几乎没有释放在场景会话期间添加的任何内存。我尝试实施我找到的文章中所有建议的解决方案,包括但不限于......

1) 确认deinit方法在 SKScene 类中调用。

2) 确认否strong对场景类中父 VC 的引用。

3)强行移除所有children和actions,并将场景设置为nil当VC消失时。 (将场景设置为nil是什么得到了deinit最终被调用的方法)

然而,尽管如此,记忆仍然存在。一些背景知识,这个应用程序介于标准 UIKit 视图控制器和 SpriteKit 场景之间(它是一个专业的绘图应用程序)。例如,应用程序在进入 SpriteKit 场景之前使用了大约 400 MB。进入场景并创建多个节点后,内存增长到超过 1 GB(到目前为止一切正常)。当我离开现场时,记忆就会消失maybe100MB。如果我重新进入场景,它会继续堆积。对于如何完全释放 SpriteKit 会话期间使用的所有内存,是否有任何方法或建议?以下是尝试解决此问题的一些方法。

SK场景类

func cleanScene() {
    if let s = self.view?.scene {
        NotificationCenter.default.removeObserver(self)
        self.children
            .forEach {
                $0.removeAllActions()
                $0.removeAllChildren()
                $0.removeFromParent()
        }
        s.removeAllActions()
        s.removeAllChildren()
        s.removeFromParent()
    }
}

override func willMove(from view: SKView) {
    cleanScene()
    self.removeAllActions()
    self.removeAllChildren()
}

介绍风险投资

var scene: DrawingScene?

override func viewDidLoad(){
    let skView = self.view as! SKView
    skView.ignoresSiblingOrder = true
    scene = DrawingScene(size: skView.frame.size)
    scene?.scaleMode = .aspectFill
    scene?.backgroundColor = UIColor.white
    drawingNameLabel.text = self.currentDrawing?.name!
    scene?.currentDrawing = self.currentDrawing!

    scene?.drawingViewManager = self

    skView.presentScene(scene)
}

override func viewDidDisappear(_ animated: Bool) {
    if let view = self.view as? SKView{
        self.scene = nil //This is the line that actually got the scene to call denit.
        view.presentScene(nil)
    }
}

正如评论中所讨论的,该问题可能与强引用循环有关。

下一步

  1. 重新创建一个简单的游戏,其中场景已正确释放,但某些节点未正确释放。
  2. 我会多次重新加载场景。您将看到场景已正确释放,但场景中的某些节点未正确释放。每次我们用新场景替换旧场景时,这都会导致更大的内存消耗。
  3. 我将向您展示如何使用 Instruments 查找问题根源
  4. 最后我将向您展示如何解决该问题。

1. 让我们创建一个有内存问题的游戏

让我们用 Xcode 创建一个基于 SpriteKit 的新游戏。

我们需要创建一个新文件Enemy.swift包含以下内容

import SpriteKit

class Enemy: SKNode {
    private let data = Array(0...1_000_000) // just to make the node more memory consuming
    var friend: Enemy?

    override init() {
        super.init()
        print("Enemy init")
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    deinit {
        print("Enemy deinit")
    }
}

我们还需要替换的内容Scene.swift具有以下源代码

import SpriteKit

class GameScene: SKScene {

    override init(size: CGSize) {
        super.init(size: size)
        print("Scene init")
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        print("Scene init")
    }

    override func didMove(to view: SKView) {
        let enemy0 = Enemy()
        let enemy1 = Enemy()

        addChild(enemy0)
        addChild(enemy1)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let newScene = GameScene(size: self.size)
        self.view?.presentScene(newScene)
    }

    deinit {
        print("Scene deinit")
    }
}

正如您所看到的,游戏的设计目的是在用户每次点击屏幕时用新场景替换当前场景。

让我们开始游戏并查看控制台。等着瞧

Scene init Enemy init Enemy init

这意味着我们总共有 3 个节点。

现在让我们点击屏幕,再次查看控制台 Scene init Enemy init Enemy init Scene init Enemy init Enemy init Scene deinit Enemy deinit Enemy deinit

我们可以看到创建了一个新场景和 2 个新敌人(第 4、5、6 行)。最后,旧场景被解除分配(第 7 行),2 个旧敌人被解除分配(第 8 行和第 9 行)。

所以内存中还有 3 个节点。而且这也好,我们没有内存韭菜。

如果我们使用 Xcode 监控内存消耗,我们可以验证每次重新启动场景时内存需求没有增加。

enter image description here

2.创建一个强引用循环

我们可以像下面这样更新 Scene.swift 中的 didMove 方法

override func didMove(to view: SKView) {
    let enemy0 = Enemy()
    let enemy1 = Enemy()

    // ☠️☠️☠️ this is a scary strong retain cycle ☠️☠️☠️
    enemy0.friend = enemy1
    enemy1.friend = enemy0
    // **************************************************

    addChild(enemy0)
    addChild(enemy1)
}

正如你所看到的,我们现在有一个敌人0和敌人1之间的强循环.

让我们再次运行游戏。

如果现在我们点击屏幕并查看控制台,我们会看到

Scene init Enemy init Enemy init Scene init Enemy init Enemy init Scene deinit

正如您所看到的,场景已被释放,但敌人不再从内存中删除。

我们来看看 Xcode 内存报告

enter image description here

现在,每次我们用新场景替换旧场景时,内存消耗都会增加。

3. 查找 Instruments 的问题

当然,我们确切地知道问题出在哪里(我们在一分钟前添加了强保留周期)。但是我们如何在一个大项目中检测到强大的保留周期呢?

单击 Xcode 中的 Instrument 按钮(当游戏在模拟器中运行时)。

enter image description here

让我们点击Transfer在下一个对话框中。

现在我们需要选择Leak Checks

enter image description here

好的,此时一旦检测到泄漏,它就会出现在 Instruments 的底部。

4.让泄漏发生

返回模拟器并再次点击。场景将再次被替换。 返回到Instruments,等几秒钟然后...

enter image description here

这是我们的泄漏。

让我们扩展一下。

enter image description here

Instruments 准确地告诉我们 8 个 Enemy 类型的对象已被泄露。

我们还可以选择视图 Cycles 和 Root 和 Instrument 将向我们展示这一点

enter image description here

这就是我们强大的保留周期!

具体来说,仪器显示 4 个强保留周期(由于我点击模拟器屏幕 4 次,总共泄漏了 8 个敌人)。

5. 解决问题

现在我们知道问题出在 Enemy 类上,我们可以返回到我们的项目并解决问题。

我们可以简单地使friend财产weak.

让我们更新一下Enemy class.

class Enemy: SKNode {
    private let data = Array(0...1_000_000)
    weak var friend: Enemy?
    ... 

我们可以再次检查以验证问题是否已消失。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

SpriteKit 没有释放所有已用内存 的相关文章

随机推荐