SpriteKit不会释放所有已用内存

The*_*oup 6 memory automatic-ref-counting retain-cycle sprite-kit swift

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

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

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

3)强制删除所有子项和动作,并将场景设置nil为VC消失时.(将场景设置为最终被调用nildeinit方法)

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

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()
}
Run Code Online (Sandbox Code Playgroud)

介绍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)
    }
}
Run Code Online (Sandbox Code Playgroud)

Luc*_*tti 12

正如评论中所讨论的,问题可能与强大的参考周期有关.

下一步

  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")
    }
}
Run Code Online (Sandbox Code Playgroud)

我们还需要用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")
    }
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,游戏旨在每次用户点击屏幕时用新的场景替换当前场景.

让我们开始游戏并看一下控制台.等着瞧

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)
}
Run Code Online (Sandbox Code Playgroud)

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

让我们再次运行游戏.

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

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

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

我们来看看Xcode Memory Report

在此输入图像描述

现在,每当我们用新的场景替换旧场景时,内存消耗就会增加.

3.找到仪器的问题

当然我们确切地知道问题的确切位置(我们在1分钟前添加了强保留周期).但是,我们怎样才能在一个大项目中发现强大的保留周期呢?

让我们点击Xcode中的乐器按钮(当游戏进入模拟器时).

在此输入图像描述

然后让我们点击Transfer下一个对话框.

现在我们需要选择 Leak Checks

在此输入图像描述

很好,此时一旦检测到泄漏,它就会出现在仪器的底部.

让我们让泄漏发生

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

在此输入图像描述

这是我们的泄漏.

让我们扩展它.

在此输入图像描述

仪器正在告诉我们8个敌人类型的物体已被泄露.

我们也可以选择视图Cycles和Root,Instrument会告诉我们这个

在此输入图像描述

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

仪器显示4个强保留周期(共有8个敌人泄漏,因为我轻敲了模拟器的屏幕4次).

5.解决问题

现在我们知道问题是Enemy类,我们可以回到我们的项目并解决问题.

我们可以简单地制作friend房产weak.

让我们更新Enemy课程.

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

我们可以再次检查以确认问题已经消失.

  • 哇。非常好的答案并写在这里。我正在寻找场景和展示 VC 之间的强引用,但不是在节点之间(我知道我有)。迫不及待想明天试试这个。谢谢! (2认同)