LoginSignup
0
0

More than 1 year has passed since last update.

GameplayKitのGKGridGraph使用時に発生したメモリーリークに対応する

Last updated at Posted at 2022-05-01

SwiftとSpriteKitでの開発中、GKGridGraphを使用しているSKSceneの遷移時にメモリーリークが発生したので原因を調査しました。

SwiftUIベースで開発しているため、エントリーポイントは次の通りです。

SandBoxApp.swift
import SwiftUI

@main
struct SandboxApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

エントリーポイントから初期表示されるSwiftUIのViewです。SpriteViewでSpriteKitのSKSceneを描画させています。

ContentView.swift
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です。

GameScene.swift
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を次の様に除去します。

GameScene.swift
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
    }
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0