Swift SpriteKit async/await bug: Memory bug with Swifts new Concurrency and SpriteKit

Viewed 110

We noticed when using async await with SpriteKit run function.

Self is retained until run action is completed which can cause memory leak. 

 Task {
            try? await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
            await controller.startIntro()
            print("intro done")
        }

If you deallocate controller, memory won't be released until await is not completed. 

calling:  scene.removeAllActions()

cause memory leak. 

Here is project reproducing steps with possible workarounds: 

https://github.com/maradic/SpriteKitConcurrencyBug

Is this SpriteKit bug or am I doing something wrong?

Original apple forum post:

https://developer.apple.com/forums/thread/701295

1 Answers

Well I guess everything here works normally. That is how await works. It waits for its async friend to finish. You have this:

Task {
        try? await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
        await controller.startIntro()
        print("intro done")
    }

This can make potentially a strong cycle cause all variables are implicitly retained inside that block. Only when Task is completed, variables will be released. If You observe better Your secondWorkAround() method, You will see that by going back in first two seconds, a retain cycle will still occur (well, view controller will be alive until SKAction doesn't end). Cause Your action is finite, thats not a biggie. But imagine if You had some sequence that runs forever.

That should be solved with [weak controller] in and after You implement that (plus remove the specific action), You will see that controller deallocates immediately after sleep is finished (after 2 seconds).

Also, doing something like scene.removeAllActions(), won't work. Cause You are removing actions that are running on scene, not on its nodes. Thus, task doesn't end when You get back to previous screen.

Now Your firstWorkAround(), well its quite hackish :) I wouldn't do that. It speeds up a scene, thus action that is run in async way from Your startIntro() method gets ended, and task is finished.

But I wouldn't go that way, it's not how is meant to work. Point is that You await the action from startIntro() to finish. That will happen if it finishes "naturally", or if You stop it. And legit way would be to find action by key and to stop it.

Note that You have something like this too:

 scene.enumerateChildNodes(withName: "//*") { (node, _) in
            node.removeAllActions()
 }

This will go recursively trough every node and remove actions for it. You can read more here. Hope this makes sense a bit, and it helps.

Related