SwiftとSpriteKitでの開発中、GKGridGraphを使用しているSKSceneの遷移時にメモリーリークが発生したので原因を調査しました。
SwiftUIベースで開発しているため、エントリーポイントは次の通りです。
import SwiftUI
@main
struct SandboxApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
エントリーポイントから初期表示されるSwiftUIのViewです。SpriteViewでSpriteKitのSKSceneを描画させています。
import SpriteKit
import SwiftUI
struct ContentView: View {
var scene: SKScene {
let scene = GameScene()
scene.scaleMode = .resizeFill
return scene
}
var body: some View {
SpriteView(scene: scene)
.ignoresSafeArea()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
問題が発生したSKSceneです。
import GameplayKit
import SpriteKit
class GameScene: SKScene {
var deltaTime: TimeInterval = 0.0
var previousTime: TimeInterval?
var grid: GKGridGraph<GKGridGraphNode>?
deinit {
print("deinit GameScene")
// (3)
self.grid = nil
}
override func didMove(to view: SKView) {
let x = Int32(15)
let y = Int32(15)
// (1)
self.grid = GKGridGraph(
fromGridStartingAt: vector_int2(x: 0, y: 0),
width: x,
height: y,
diagonalsAllowed: false
)
}
override func update(_ currentTime: TimeInterval) {
if let previousTime = previousTime {
self.deltaTime += currentTime - previousTime
}
if self.deltaTime > 1 {
print(self.deltaTime)
self.deltaTime = 0.0
// (2)
let scene = GameScene()
scene.scaleMode = .resizeFill
self.view?.presentScene(scene)
}
self.previousTime = currentTime
}
}
大まかな処理の流れは、次の通りです。
(1) SKSceneのインスタンスプロパティとして、GKGridGraphを作成します。
(2) テストのため、1秒置きに新しいSKSceneのインスタンスを作成し、SKViewの presentScene(scene)
メソッドで画面を遷移します。
(3) SKSceneの deinit
時にGKGridGraphは合わせて破棄されるため、本来は不要ですが、念のため、GKGridGraphに nil
を代入します。
この処理を実行し続けるとメモリリークが発生し、最終的にアプリケーションがハングアップします。
結論を述べると、SKSceneの deinit
時にインスタンスプロパティのGKGridGraphは合わせて破棄されますが、そのGKGridGraphに埋められているGKGridGraphNodeがメモリ上に残り続けてしまうため、メモリリークが発生していました。
これは、Xcodeで処理を実行し、次のメモリ状態に注目することで確認できました。
- CoreFoundationのGKGridGraphに紐づくNSMutableArrayが増え続けている。
- GamePlaykitのGKGridGraphNodeが増え続けている。
注意として、このテストコードでは、ContentView.swift内でSpriteViewに初回に表示されるSKSceneを紐づけているため、初回に表示されるSKSceneインスタンスに紐づく変数は残り続けます。従って、3回目の画面遷移以降からメモリリークとして確認できます。
この事象へ対策するには、画面遷移の前にGKGridGraphに埋められているGKGridGraphNodeのクリーンアップを実行します。
具体的には、GKGridGraphからすべてのGKGridGraphNodeを次の様に除去します。
import GameplayKit
import SpriteKit
class GameScene: SKScene {
var deltaTime: TimeInterval = 0.0
var previousTime: TimeInterval?
var grid: GKGridGraph<GKGridGraphNode>?
deinit {
print("deinit GameScene")
// (3)
+ if let grid = self.grid {
+ grid.remove(grid.nodes!)
+ }
self.grid = nil
}