Memory Problems When Switching Between Scenes Spritekit

Memory leak when presenting SpriteKit scenes

First off you are presenting SKScene's correctly and you are correct you should be able to trust that the old scene will get cleaned up. By that I mean there is nothing else you need to do to make it release.

Now with that being said there are things that you may have done to create a circular reference. Hopefully a couple of these checks will help you track it down.

The first place I always look is at your player. If your scene has a player property and your player has a scene property this could prevent the scene from deallocating. Player holds on to the scene and scene holds on to player. I doubt this is your case but worth checking.

Second place to look is there anything else that has a reference or property to that scene? A common issue is when you create your own delegate method. If your player does an action and then calls a method back to that scene. The player should only have weak references to that scene. I have done this myself and seen other do it where they create a custom delegate or protocol and keep a strong reference to it instead of weak.

Third place to look is anywhere in your code where you call self. This is common in run blocks of SKActions. You may have an action that calls a method on the scene and you may have a property on that scene of that SKAction. The action has reference to the scene due to the run block and the scene has a reference to the action. So look for self and see if that object is a property on that scene.

Hopefully that helps you track it down. I know it can be frustrating tracking a leak like this down and those are common issues I have seen in the past.

SpriteKit Memory increase every time new scene presented

I tried checking if things had not been deallocated and this did not solve the problem. I used a developer technical support ticket and an engineer advised me to turn off "GPU Frame Capture" in the scheme for the project.

This 95% solved the problem. Memory usage has reduced to a lot more reasonable amount and the app no longer continues to build up memory usage after I implement reasonable methods to deallocate scenes, nodes etc...

I asked if this solution was only for testing in Xcode and I was told it was not, this is how my app will perform on the App Store:

"GPU Frame Capture is a tool for debugging and is only present when running your app with the Xcode Debugger attached!" - Said the engineer.

SpriteKit game freezing after switching scenes multiple times

By referencing self.destY in the motionManager.startAccelerometerUpdatesToQueue closure, you are creating a strong reference cycle. From the docs,

If you assign a closure to a property of a class instance, and the closure captures that instance by referring to the instance or its members, you will create a strong reference cycle between the closure and the instance.

This reference cycle is preventing your scene from being released. And since you do not stop the motion manager, the old managers (plural) are still running when you transition scenes. This is likely causing the current scene to freeze after multiple transitions.

Swift uses capture lists to avoid strong reference cycles, where a capture list has the form

{ [ /* weak or unowned + object, ...*/ ]
/* parameters */ in

}

You can define the capture in a closure as unowned or weak. From the docs,

Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.

and

Conversely, define a capture as a weak reference when the captured reference may become nil at some point in the future. Weak references are always of an optional type, and automatically become nil when the instance they reference is deallocated. This enables you to check for their existence within the closure’s body.

Here's an example of how to add a capture list to your accelerometer handler:

    motionManager.accelerometerUpdateInterval = 0.1
motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.currentQueue()) {
[unowned self] accelerometerData, error in

var currentY = self.enemySprite.position.y
let acceleration = accelerometerData.acceleration
self.destY = (CGFloat(acceleration.y) * 0.75) + (self.destY * 0.25)
}

Lastly, it's a good idea to stop the accelerometer updates before transitioning scenes

    override func willMoveFromView(view: SKView) {
motionManager.stopAccelerometerUpdates()
}

Why does SpriteKit crash when transitioning back and forth between scenes

I found out the mistake I was making.

I was instantiating the nodes outside my scene 2 class, making them global. Therefore when I went from scene 1 -> scene 2 -> scene 1 there was not problem but going then to scene 2 again caused a crash because the nodes were globally created.

Solution:

Moving the following code within the class solved the problem.

var mainLayer = SKSpriteNode()
var labelHolderLayer = SKSpriteNode()
var livesDisplay = SKSpriteNode()
var scoreLabel = SKLabelNode()

SpriteKit not deallocating all used memory

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.

Sample Image

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

Sample Image

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

Sample Image

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

Now we need to select the Leak Checks

Sample Image

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

Sample Image

Here it is our leak.

Let's expand it.

Sample Image

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

Sample Image

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.



Related Topics



Leave a reply



Submit