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

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

问题描述

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

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)确认在SKScene类中调用了deinit方法.

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

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

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

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

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)

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

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类

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

展示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.

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

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

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

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

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

我们需要创建一个具有以下内容的新文件Enemy.swift

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

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

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

Scene init Enemy init Enemy init

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

It means we have a total of 3 nodes.

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

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

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

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

因此,我们的内存中仍然有3个节点.很好,我们没有记忆韭菜.

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

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

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.

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

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

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

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

让我们再次运行游戏.

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

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

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内存报告

Let's look at Xcode Memory Report

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

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

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

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?

在游戏运行到模拟器中时,单击Xco​​de中的乐器"按钮.

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

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

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

现在我们需要选择Leak Checks

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

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

4.让泄漏发生

返回模拟器,然后再次点击.场景将再次被替换. 返回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...

这是我们的泄漏.

展开它.

Instruments准确地告诉我们,有8个Enemy类型的对象已泄漏.

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!

特别是仪器显示了4个强保持周期(总共泄漏了8个敌人,因为我轻按了模拟器的屏幕4次).

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.解决问题

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

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.

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

We can simply make the friend property weak.

让我们更新Enemy类.

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天全站免登陆