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. 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 contentWe also need to replace the content of
Scene.swift
with the following source codeLet'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
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
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
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!
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
propertyweak
.Let's update the
Enemy
class.We can check again to verify the problem is gone.