Spritekit Not Deallocating All Used Memory

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.

SKScene is not deallocating in xcode

As it turns out, I had some properties that had strong references when they should have been weak. This was causing the scene to be retained as the properties inside of it were not deallocating....

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.

Memory increase when returning to GameScene

It's hard to actually give you a solution not being able to debug with you and see how you are managing references in your Game, but I've encoutered this issue and I was able to solve it using those two things:


deinit {
print("(Name of the SKNode) deinit")
}

Using this, I could find which objects the arc could not remove reference.


 let color4 = SKAction.run { [weak self] in
self?.label.fontColor = .green
}

This one helped me clean all strong references in my Game. For many of us, it’s best practice to always use weak combined with self inside closures to avoid retain cycles. However, this is only needed if self also retains the closure. More Information.


Memory Increase in SpriteKit even though removeAllChildren called

You already found the problem:

Deinit is not being called after a scene changes.

If when a scene does change the deinit of the old scene is not called then that's the problem. It means that the old scene is still in memory.

This is probably happening because you have a strong retain cycle. This means that a child (or a child of a child...) of your scene has a strong reference to the scene itself.

You must find that reference and declare it with the weak keyword.

Deallocate SKScene after transition to another SKScene in SpriteKit

A similar problem was faced by the person who asked this question.

When asked whether he was able to solve it, they said:

Yes, I did, there wasn't anything I could do about it from the scene
or Sprite Kit for that matter, I simply needed to remove the scene and
the view containing it completely from the parent view, cut all its
bonds to the other parts of the system, in order for the memory to be
deallocated as well.

You should use separate views for each of your scene and transition between these views. You can follow these steps to make it look natural:

1 - At the point you want to transition from one scene to the other, take a snapshot of the scene using the following code:

UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, scale);
[self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

Then, add this image as a subview of the SKView of the current scene, and remove the scene.

2 - Initialise the new scene on another view, and transition between the two views using UIView animation.

3 - Remove the first view from it's superview.

How to deallocate unused SKScene

In order to deallocate you SKScene instance -> you Should Eliminate every pointer that retains it -> then ARC will automagically release it.

Simply call the [skView presentScene:nil]; method which also will remove the SCScene and deallocate it by setting SKView.scene property to nil.

SKView *skView = (SKView *)self.view; 
GameScene *scene = (GameScene *)skView.scene;

for(SKNode * child in scene.children)
{
[child removeAllActions];
}
[scene removeAllChildren];
[scene removeAllActions];
[skView presentScene:nil];

Note Normally you don't have to remove everything from the SKScene if your memory is managed right just call [skView presentScene:nil]; method and ARC will take care of it.

Apparently you had something inside the SKScene that retained it.
So by removing everything from it, we eliminated the retain cycle.

This solution will not always work, it will work only if one of the SKActions or SKNodes are retaining the SKScene which were the issue in your case, However your SKScene could be retained from somewhere else and this answer wouldn't really help you



Related Topics



Leave a reply



Submit