SpriteKit not deallocating all used memory
As discussed in the comments, the problem is probably related to a strong reference cycle.
Next steps
- Recreate a simple game where the scene is properly deallocated but some of the nodes are not.
- 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.
- I'll show you how to find the origin of the problem with Instruments
- 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.
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.
Swift: Deallocate GameScene after transition to new scene?
In my project, I used the following mechanism and it worked well. All my scenes SKScene
objects were optional variables. When i need the new scene to show, i create it and present at SKView
. When i need to display the new scene, I set the previous object scene object to nil
, this immediately reducing the reference count by 1, and becouse of at this moment no one object is not use my scene, the reference count becomes zero and scene was deleted.
The SKScene
object is a ordinary class object and ARC
works with them like with all reference type objects. You only need to monitor the number of references to the scene. All are finished with the various resources I was start in deinit
of SKScene
object
The simple example:
At UIViewController
we have optional objects of GameScene
:
class GameViewController: UIViewController {
var scene1: GameScene?
var scene2: GameScene?
var skView: SKView?
// Action to present next Scene
@IBAction func nextSceneAction(sender: AnyObject) {
print("Next scene")
// Create new GameScene object
scene2 = GameScene(fileNamed:"GameScene")
// Present scene2 object that replace the scene1 object
skView!.presentScene(scene2)
scene = nil
}
override func viewDidLoad() {
super.viewDidLoad()
// Create GameScene object
scene = GameScene(fileNamed:"GameScene")
skView = (self.view as! SKView)
skView!.showsFPS = true
skView!.showsNodeCount = true
skView!.ignoresSiblingOrder = true
scene!.scaleMode = .AspectFill
// Present current scene
skView!.presentScene(scene)
}
}
At GameScene
in deinit
print some text to show that it object will be deleted:
class GameScene: SKScene {
...
deinit {
print("Deinit scene")
}
}
Debug output after push the nextSceneAction
button:
Next scene
Deinit scene
Managing memory in Swift (SceneKit game)
I also meet the same case.And I find a solution to fix the bug.You must asynchronous to remove and release scnView.Just look the following code.
_scnView.antialiasingMode = SCNAntialiasingModeNone;
__block SCNView *strongScnView = _scnView;
_scnView = nil;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[strongScnView setScene:nil];
[strongScnView removeFromSuperview];
[strongScnView stop:nil];
strongScnView = nil;
});
Related Topics
Programmatically Creating Constraints Bound to View Controller Margins
How Set Rootviewcontroller in Scene Delegate iOS 13
Implementing Nscopying in Swift with Subclasses
Grab Frames from Video Using Swift
In Swift, Can You Split a String by Another String, Not Just a Character
In Swiftui, Where Are the Control Events, I.E. Scrollviewdidscroll to Detect the Bottom of List Data
Get Image from Calayer or Nsview (Swift 3)
Background Request Not Execute Alamofire Swift
Difference Between Associated and Raw Values in Swift Enumerations
How to Execute Code Once and Only Once in Swift
What Determines Whether a Swift 5.5 Task Initializer Runs on the Main Thread
How to Disable Vertical Scroll in Tabview with Swiftui
Swiftier Swift for 'Add to Array, or Create If Not There...'
Exporting Mp4 Through Avassetexportsession Fails
How Do People Deal with Iterating a Swift Struct Value-Type Property