2

I currently have an Assets singleton class that provides me access to textures, sounds, and music. As my partner and I are going through the memory management stage of our project we have realized that we may have a serious leak being created, and based on my use of Xcode instruments our biggest issue may center around this singleton class. While there are certainly other leaks present, we have noticed that when moving between map screen and game screen back and forth, there is a fairly steady increase of ~100 mb, which appears to correspond to our 11 map assets. With that context, my question is this:

Would the code below create a retain cycle, and if so, can it be managed with the existence of the singleton class, or ought we break this thing up s.t. texture atlases are held separately?

func transitionToMapScreen()
    {
        //I hope this isn't necessary eventually, but we were trying to ensure all game textures and emitters were deallocated
        Assets.sharedInstance.deallocateGameAssets()

        gameScene = GameScene()

        Assets.sharedInstance.preloadMap
        {
            [unowned self] in

            let mapScene = MapScreen(fileNamed: "MapScreen")!
            mapScene.preCreate()
            mapScene.scaleMode = self.scaleMode

                // Transition with a fade animation
                let reveal = SKTransition.fade(withDuration: 2.0)

                let fadeMusic = SKAction.run
                {
                    Assets.sharedInstance.bgmTitlePlayer?.setVolume(1.0, fadeDuration: 1.0)

                    Assets.sharedInstance.bgmTitlePlayer?.play()

                    Assets.sharedInstance.bgmGamePlayer?.setVolume(0.0, fadeDuration: 1.0)
                }

                let stopGameMusic = SKAction.run
                {
                    Assets.sharedInstance.bgmGamePlayer?.stop()
                }

                let transitionAction = SKAction.run
                {
                    self.view?.presentScene(mapScene, transition: reveal)
                }

                self.run(SKAction.sequence([SKAction.wait(forDuration: 1.0), fadeMusic, SKAction.group([stopGameMusic, transitionAction])]))

        } // end Assets.sharedInstance.preloadMap completion block*/
    }

From what I understand about retain cycles in Swift, isn't this creating a self reference to the Assets class and creating a memory leak? And could this explain the behavior of our map assets being retained in memory? And if so, what is the proper method of managing this?

Mike Pandolfini
  • 532
  • 4
  • 17
  • 4
    If there is a retain cycle and leak, Instruments or the Memory Graph will tell you. – matt Jan 30 '19 at 17:18
  • 1
    I'm not sure, but it's possible your Assets.sharedInstance is retaining itself. – kid_x Jan 30 '19 at 17:49
  • 1
    matt, I do see leaks pertaining to this but bytes used is quite small. Perhaps this is my unfamiliarity with Allocations, but does this indicate that if I am seeing memory usage rise dramatically, leaks in the < 25 kB bytes used over a 4-5 minute game session are NOT my issue? – Mike Pandolfini Jan 30 '19 at 17:54
  • Again, you cannot be in any doubt about what is dramatically using memory. Instruments just tells you. No need to speculate. – matt Jan 31 '19 at 04:58
  • 1
    Sure, I agree it does. But the issue is that I'm not exactly sure of WHY it is happening because SK has some confusing behavior. In particular, I see certain preloading not occurring when entering certain scenes once said preloading has been completed despite deallocating these assets prior. This is confusing, but after some further reading on this it seems SK will sometimes keep assets in memory in a lazy fashion with intention. So I am seeing this sort of behavior, I do not see leaks associated with it, but I do see memory usage rising dramatically in instruments across my texture atlases. – Mike Pandolfini Feb 01 '19 at 05:41
  • 1
    Just to clarify: my current guess, based on Instruments, is that this isn't a retain cycle issue of the Assets singleton (which we eliminated by the way -- apparently singletons are evil according to everyone), but rather an intentional retain of certain assets by SK once they are preloaded. We have large texture atlases which we are shrinking to test the theory. This post in particular I thought was useful on the topic: https://stackoverflow.com/questions/37119707/how-does-sktexture-caching-and-reuse-work-in-spritekit – Mike Pandolfini Feb 01 '19 at 05:47

1 Answers1

1

I wanted to post this here for those who might be looking for an answer to a similar problem related to hunting down retain cycles to explain memory growth issues. First, thanks very much to all those who helped me halt my neurons' random frantic attempts to find retain cycles where there were none (or were not ones big enough to matter). Now:

First, retain cycles are scary, sure, but using instruments to find them and then managing as Apple recommends since Swift 4.2 is appropriate:

something()
{
    [weak self] in 

    guard let self = self else { return }

    self.whatever()
}

I've seen some people argue that you should determine whether unowned or weak makes sense -- this honestly takes the guesswork out and is much easier. I did find unowned crashes are rare anyway at least for us, but I won't opine on your app, and this solves things. Now, then, once you have cleaned house:

We discovered that our memory growth issues stemmed not from our asset singleton class inherently but rather the sheer size of our texture atlases and corresponding overlapping usage of those atlases. I cannot recommend this discussion enough: How does SKTexture caching and reuse work in SpriteKit?. That will explain conceptually the issues you might face with atlases better than I can.

Summarily, however: SpriteKit manages the allocation of your texture atlases, and accordingly you must understand that if you have a very large atlas that is frequently loaded it may not manage it as you expect (I still don't have enough detail to describe that in a better way, but as I said please reference the discussion above as well as Apple's developer guide on SKTextureAtlas: https://developer.apple.com/documentation/spritekit/sktextureatlas).

Now, related to that Apple discussion, I noted this line, which I think really ought to be bolded and in red: "SpriteKit implicitly loads an atlas when one of the atlas's textures is accessed." This was critical to solving what I think was our underlying issue: we had a few places where we accessed textures in the atlas via a single instance for some reason -- you must recognize that in the case of a large atlas SpriteKit will then load your entire massive atlas into memory. So I no longer take lightly Apple's note to manage your atlas sizes. Atlases are meant for assets that are always used together and are going to be drawn together. Loading disparate assets into an atlas was our mistake. We are reorganizing how we manage these accordingly.

Mike Pandolfini
  • 532
  • 4
  • 17