SpriteKit 没有释放所有使用的内存 [英] SpriteKit not deallocating all used memory

查看:25
本文介绍了SpriteKit 没有释放所有使用的内存的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

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

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

SKScene 类

func cleanScene() {如果让 s = self.view?.scene {NotificationCenter.default.removeObserver(self)自己的孩子.forEach {$0.removeAllActions()$0.removeAllChildren()$0.removeFromParent()}s.removeAllActions()s.removeAllChildren()s.removeFromParent()}}覆盖函数 willMove(从视图:SKView){清洁场景()self.removeAllActions()self.removeAllChildren()}

介绍风投

var 场景:绘图场景?覆盖 func viewDidLoad(){让skView = self.view as!SKViewskView.ignoresSiblingOrder = true场景=绘图场景(​​大小:skView.frame.size)场景?.scaleMode = .aspectFill场景?.backgroundColor = UIColor.whitedrawingNameLabel.text = self.currentDrawing?.name!场景?.currentDrawing = self.currentDrawing!场景?.drawingViewManager = selfskView.presentScene(场景)}覆盖 func viewDidDisappear(_ animated: Bool) {if let view = self.view as?SKView{self.scene = nil//这是真正让场景调用 denit 的行.view.presentScene(nil)}}

解决方案

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

后续步骤

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

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

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

我们需要新建一个文件Enemy.swift,内容如下

导入SpriteKit类敌人:SKNode {private let data = Array(0...1_000_000)//只是为了让节点更消耗内存var朋友:敌人?覆盖初始化(){超级初始化()print("敌人初始化")}需要初始化?(编码器 aDecoder:NSCoder){fatalError("init(coder:) 尚未实现")}初始化{print("敌人消灭")}}

我们还需要将Scene.swift的内容替换成如下源码

导入SpriteKit类游戏场景:SKScene {覆盖初始化(大小:CGSize){超级初始化(大小:大小)print("场景初始化")}需要初始化?(编码器 aDecoder:NSCoder){super.init(编码器:aDecoder)print("场景初始化")}覆盖 func didMove(查看:SKView){让敌人0 =敌人()让敌人1 =敌人()addChild(enemy0)addChild(enemy1)}覆盖 func touchesBegan(_ touches: Set, with event: UIEvent?) {让 newScene = GameScene(size: self.size)self.view?.presentScene(newScene)}初始化{print("场景初始化")}}

<块引用>

如您所见,该游戏旨在每次用户点击屏幕时将当前场景替换为新场景.

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

<代码>场景初始化敌人初始化敌人初始化

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

现在让我们点击屏幕并再次查看控制台<代码>场景初始化敌人初始化敌人初始化场景初始化敌人初始化敌人初始化场景初始化敌人deinit敌人deinit

我们可以看到创建了一个新场景和 2 个新敌人(第 4、5、6 行).最后,旧场景被释放(第 7 行),2 个旧敌人也被释放(第 8 行和第 9 行).

所以我们在内存中还有 3 个节点.这很好,我们没有内存韭菜.

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

2.让我们创建一个强引用循环

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

覆盖 func didMove(查看: SKView) {让敌人0 =敌人()让敌人1 =敌人()//☠️☠️☠️这是一个可怕的强保留周期☠️☠️☠️敌人0.朋友=敌人1敌人1.朋友=敌人0//**************************************************addChild(enemy0)addChild(enemy1)}

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

让我们再次运行游戏.

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

<代码>场景初始化敌人初始化敌人初始化场景初始化敌人初始化敌人初始化场景初始化

<块引用>

如您所见,场景已被释放,但敌人不再从内存中删除.

让我们看看 Xcode 内存报告

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

3.使用 Instruments 查找问题

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

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

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

现在我们需要选择Leak Checks

<块引用>

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

4.让我们让泄漏发生

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

这是我们的泄漏.

让我们展开它.

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

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

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

<块引用>

具体而言,仪器显示 4 个强保留周期(总共有 8 个敌人泄漏,因为我点击了模拟器的屏幕 4 次).

5.解决问题

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

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

让我们更新 Enemy 类.

类敌人:SKNode {私有数据 = 数组(0...1_000_000)弱变朋友:敌人?...

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

I have ready many (if not all) articles on SO and other sites about the disasters of dealing with SpriteKit and memory issues. My problem, as many others have had, is after i leave my SpriteKit scene barely any of the memory added during the scene session is released. I've tried to implement all suggested solutions in the articles i've found, including, but not limited to...

1) Confirm the deinit method is called in the SKScene class.

2) Confirm no strong references to the parent VC in the scene class.

3) Forcefully remove all children and actions, and set the scene to nil when the VC disappears. (Setting the scene to nil was what got the deinit method to eventually get called)

However, after all of that, memory still exists. Some background, this app goes between standard UIKit view controllers and a SpriteKit scene (it's a professional drawing app). As an example, the app is using around 400 MB before entering a SpriteKit scene. After entering the scene and creating multiple nodes, the memory grows to over 1 GB (all fine so far). When i leave the scene, the memory drops maybe 100 MB. And if i re-enter the scene, it continues to pile on. Are there any ways or suggestions on how to completely free all memory that was used during a SpriteKit session? Below is a few of the methods being used to try and fix this.

SKScene class

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

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

解决方案

As discussed in the comments, the problem is probably related to a strong reference cycle.

Next steps

  1. Recreate a simple game where the scene is properly deallocated but some of the nodes are not.
  2. I'll reload the scene several time. You'll see the scene is properly deallocated but some nodes into the scene are not. This will cause a bigger memory consumption each time we replace the old scene with a new one.
  3. I'll show you how to find the origin of the problem with Instruments
  4. And finally I'll show you how to fix the problem.

1. Let's create a game with a memory problem

Let's just create a new game with Xcode based on SpriteKit.

We need to create a new file Enemy.swift with the following content

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

We also need to replace the content of Scene.swift with the following source code

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.

Let's start the game and look at the console. Will' see

Scene init Enemy init Enemy init

It means we have a total of 3 nodes.

Now let's tap on the screen and let's look again at the console Scene init Enemy init Enemy init Scene init Enemy init Enemy init Scene deinit Enemy deinit Enemy deinit

We can see that a new scene and 2 new enemies have been created (lines 4, 5, 6). Finally the old scene is deallocated (line 7) and the 2 old enemies are deallocated (lines 8 and 9).

So we still have 3 nodes in memory. And this is good, we don't have memory leeks.

If we monitor the memory consumption with Xcode we can verify that there is no increase in the memory requirements each time we restart the scene.

2. Let create a strong reference cycle

We can update the didMove method in Scene.swift like follows

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

As you can see we now have a strong cycle between enemy0 and enemy1.

Let's run the game again.

If now we tap on the screen and the look at the console we'll see

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.

Let's look at Xcode Memory Report

Now the memory consumption goes up every time we replace the old scene with a new one.

3. Finding the issue with Instruments

Of course we know exactly where the problem is (we added the strong retain cycles 1 minute ago). But how could we detect a strong retain cycle in a big project?

Let click on the Instrument button in Xcode (while the game is running into the Simulator).

And let's click on Transfer on the next dialog.

Now we need to select the Leak Checks

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

4. Let's make the leak happen

Back to the simulator and tap again. The scene will be replaced again. Go back to Instruments, wait a few seconds and...

Here it is our leak.

Let's expand it.

Instruments is telling us exactly that 8 objects of type Enemy have been leaked.

We can also select the view Cycles and Root and Instrument will show us this

That's our strong retain cycle!

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. Fixing the problem

Now that we know the problem is the Enemy class, we can go back to our project and fix the issue.

We can simply make the friend property weak.

Let's update the Enemy class.

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

We can check again to verify the problem is gone.

这篇关于SpriteKit 没有释放所有使用的内存的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆