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

标签 swift memory sprite-kit automatic-ref-counting retain-cycle

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

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

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

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

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

SKScene 类

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

让我们用基于 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.

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

场景初始化 敌人初始化 敌人初始化

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

现在让我们点击屏幕并再次查看控制台 场景初始化 敌人初始化 敌人初始化 场景初始化 敌人初始化 敌人初始化 场景初始化 敌人deinit 敌人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 之间有一个强循环

让我们再次运行游戏。

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

场景初始化 敌人初始化 敌人初始化 场景初始化 敌人初始化 敌人初始化 场景初始化

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

让我们看看 Xcode 内存报告

enter image description here

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

3。使用 Instruments 查找问题

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

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

enter image description here

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

现在我们需要选择Leak Checks

enter image description here

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

4。让我们让泄漏发生

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

enter image description here

这是我们的泄漏。

让我们展开它。

enter image description here

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

我们还可以选择 View Cycles 和 Root 和 Instrument 将向我们展示这个

enter image description here

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

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 类上,我们可以回到我们的项目并解决问题。

我们可以简单地将 friend 属性设为 weak

让我们更新 Enemy 类。

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

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

关于swift - SpriteKit 没有释放所有使用的内存,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47663042/

相关文章:

swift - Swift 语言中的 IIFE

swift - 为什么有时我们在 swift 中使用 -> void 作为完成处理程序,但有时却不用?

ios - 如何在 iOS 12 中以编程方式终止应用程序

swift - Sprite Kit physicsBody.resting 行为

swift - SKSpriteNode - 中断 animateWithTextures 后纹理未恢复到原始大小

swift - 如何使用 swift 使车辆或火车在 iOS Sprite Kit Game 中的场景中移动

ios - swift:将字符串转换为 double 时出现问题

c# - 变量的内存地址

c++ - 内存不足的概念

C++对象实例成员函数内存表示