SpriteKit 没有释放所有使用的内存

SpriteKit not deallocating all used memory

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

1) 确认在SKScene class.

中调用了deinit方法

2) 确认在场景 class.

中没有 strong 对父 VC 的引用

3) 强制移除所有children和actions,当VC消失时将场景设置为nil。 (将场景设置为 nil 是最终调用 deinit 方法的原因)

不过,经历了这么多,记忆还是存在的。一些背景,这个应用程序介于标准 UIKit 视图控制器和 SpriteKit 场景之间(它是一个专业的绘图应用程序)。例如,应用程序在进入 SpriteKit 场景之前使用了大约 400 MB。进入场景并创建多个节点后,内存增长到 1 GB 以上(目前一切正常)。当我离开现场时,内存下降可能 100 MB。如果我重新进入现场,它会继续堆积。关于如何完全释放 SpriteKit 会话期间使用的所有内存,有什么方法或建议吗?以下是一些用于尝试解决此问题的方法。

SKScene class

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

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

呈现VC

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。让我们创建一个有内存问题的游戏

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

我们需要创建一个包含以下内容的新文件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")
    }
}

As you can see the game is designed to replace the current scene with a new one each time the user taps the screen.

让我们开始游戏,看看控制台。会看到

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监控内存消耗,我们可以验证每次重启场景时内存需求没有增加。

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)
}

如您所见,我们现在在 enemy0 和 enemy1 之间有一个 强循环

我们再运行游戏吧。

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

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

As you can see the Scene is deallocated but the Enemy(s) are no longer removed from memory.

让我们看看Xcode内存报告

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

3。查找 Instruments

的问题

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

让我们点击 Xcode 中的 Instrument 按钮(当游戏 运行 进入模拟器时)。

然后在下一个对话框中单击 Transfer

现在我们需要 select Leak Checks

Good, at this point as soon as a leak is detected, it will appear in the bottom of Instruments.

4。让泄漏发生

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

这是我们的漏洞。

让我们扩大它。

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

我们还可以 select 视图 Cycles and Root and Instrument 会向我们展示这个

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

Specifically Instrument is showing 4 Strong Retain Cycles (with a total of 8 Enemy(s) leaked because I tapped the screen of the simulator 4 times).

5。解决问题

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

我们可以简单地使 friend 属性 weak.

让我们更新 Enemy class。

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

我们可以再次检查以确认问题已解决。