I discovered earlier that my memory usage in my game was only going up when the tiles were moved, but it never went back down again. From this, I could tell there was a memory leak.
I then started using Xcode Instruments, which I am very new to. So I followed many things from this article , especially the Recording Options
, and then I set the mode to show the Call Tree
.
I have two functions that just move all the tiles along that row/column, and then clones the tile at the end (using node.copy()
) so everything can "loopover", hence the project name.
I feel as if the tile cloning may be causing some retention cycle, however, it is stored in the variable within the function scope. After I run the SKAction
on the clone, I remove the tile from the scene using copiedNode.removeFromParent()
.
So what may be causing this memory leak? Could I be looking in the wrong place?
I have shortened this code to what I consider necessary.
Declaration at the top of the class:
/// Delegate to the game scene to reference properties.
weak var delegate: GameScene!
/// All the cloned tiles currently on the board.
private var cloneTiles = [SKSpriteNode]()
Cloning of the tile within the moving tiles functions:
/// A duplicate of the current tile.
let copiedNode = currentTile.node.copy() as! SKSpriteNode // Create copy
cloneTiles.append(copiedNode) // Add as a clone
delegate.addChild(copiedNode) // Add to the scene
let copiedNodeAction = SKAction.moveBy(x: movementDifference, y: 0, duration: animationDuration) // Create the movement action
// Run the action, and then remove itself
copiedNode.run(copiedNodeAction) {
self.cloneTiles.remove(at: self.cloneTiles.firstIndex(of: copiedNode)!)
copiedNode.removeFromParent()
}
Function to move tiles immediately:
/// Move all tiles to the correct location immediately.
private func moveTilesToLocationImmediately() {
// Remove all clone tiles
cloneTiles.forEach { $0.removeFromParent() }
cloneTiles.removeAll()
/* Moves tiles here */
}
Is there something I need to declare as a weak var
or something? I know how retain cycles occur, but do not get why it exists in this code, as I remove the cloned tile reference from the cloneTiles
array.
Mark Szymczyk
) Here is what happened after I double-clicked on the move tiles function in the call stack (refer to his answer below):
This is confirming that the memory leak is caused somehow by the node clone, but I still don't know why this node is still being retained after it is removed from the cloneTiles
array and the scene. Could the node be having trouble getting removed from the scene for some reason?
Please leave any tips or questions about this, so this problem can be solved!
I have now been trying to get to grips with Xcode Instruments, but I am still really struggling to find this memory leak. Here is the leaks panel which may help:
Even after trying [weak self]
, I still had no luck:
Even the leaks history still looks the same with the [weak self]
within the closure.
Currently, @matt
is helping me with this issue. I have changed a few lines of code, by adding things like [unowned self]
:
// Determine if the tile will roll over
if direction == .up && movementDifference < 0 || direction == .down && movementDifference > 0 {
// Calculate where the clone tile should move to
movementDifference -= rollOverDistance
/// A duplicate of the current tile.
let copiedNode = currentTile.node.copy() as! SKSpriteNode // Create copy
cloneTiles.append(copiedNode) // Add as a clone
delegate.addChild(copiedNode) // Add to the scene
let copiedNodeAction = SKAction.moveBy(x: 0, y: movementDifference, duration: animationDuration) // Create the movement action
// Run the action, and then remove itself
copiedNode.run(copiedNodeAction) { [unowned self, copiedNode] in
self.cloneTiles.remove(at: self.cloneTiles.firstIndex(of: copiedNode)!).removeFromParent()
}
// Move the original roll over tile back to the other side of the screen
currentTile.node.position.y += rollOverDistance
}
/// The normal action to perform, moving the tile by a distance.
let normalNodeAction = SKAction.moveBy(x: 0, y: movementDifference, duration: animationDuration) // Create the action
currentTile.node.run(normalNodeAction) { [unowned self] in // Apply the action
if forRow == 1 { self.animationsCount -= 1 } // Lower animation count for completion
}
Unfortunately, I could not make copiedNode
a weak
property as it would always be instantly nil
, and unowned
caused a crash about the reference being read after being deallocated. Here is also the Cycles & Roots
graph if this is helpful:
Thank you for any help!
I can help a little on the Instruments front. If you double-click the moveHorizontally
entry in the Instruments call tree, Instruments will show you the lines of code that are allocating the leaked memory. At the bottom of the window is a Call Tree button. If you click on that, you can invert the call tree and hide system libraries. Doing that will make it easier to find your code in the call tree.
You can learn more about Instruments in the following article:
I'm rather suspicious of the way you're managing the copied node; you may be releasing it prematurely, and only the retain cycle was preventing you from discovering this mistake. However, let's concentrate on breaking the retain cycle.
What you want to do is make everything coming into the action method weak
, so that there is no strong capture by the action method. Then in the action method you want to immediately retain those weak references so they don't vanish out from under you. That's called the "weak-strong dance". Like this:
copiedNode.run(copiedNodeAction) { [weak self, weak copiedNode] in
if let `self` = self, let copiedNode = copiedNode {
// do stuff here
// be sure to log so you know we arrived here at all, as we might not
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.