sprite-kitsktexturesktextureatlas

How does SKTexture caching and reuse work in SpriteKit?


It is often mentioned on StackOverflow that SpriteKit does its own internal caching and reuse.

If I don't make any effort to reuse textures or atlases, what caching and reuse behavior can I expect from SpriteKit?


Solution

  • Texture Caching in SpriteKit

    “Long story short: rely on Sprite Kit to do the right thing for you.” -@LearnCocos2D

    Here’s the long story, as of iOS 9.

    The texture’s bulky image data is not kept directly on the SKTexture object. (See the SKTexture class reference.)

    SpriteKit has some built-in caching for the texture’s bulky image data. Two features:

    1. The texture’s bulky image data is cached by SpriteKit until SpriteKit feels like getting rid of it.

      • According to the SKTexture class reference: “Once the SKTexture object is ready for rendering, it stays ready until all strong references to the texture object are removed.”

      • In current iOS, it tends to stick around longer, perhaps even after the whole scene is gone. A StackOverflow comment quotes an Apple tech support: “iOS releases the memory cached by +textureWithImageNamed: or +imageNamed: when it sees fit, for instance when it detects a low-memory condition.”

      • In the simulator, running a test project, I was able to see texture memory reclaimed immediately after a removeFromParent. Running on a physical device, though, the memory seemed to linger; repeatedly rendering and deallocating the texture resulted in no additional disk accesses.

      • I wonder: Could the rendering memory be released early in some memory-critical situations (when the texture is retained but not currently displayed)?

    2. SpriteKit reuses the cached bulky image data smartly.

      • In my experiments, it was hard not to reuse it.

      • Say you’ve got a texture displaying in a sprite node, but rather than reusing the SKTexture object, you call [SKTexture textureWithImageNamed:] for the same image name. The texture will not be pointer-identical to the original texture, but it will share the bulky image data.

      • The above is true whether the image file is part of an atlas or not.

      • The above is true whether the original texture was loaded using [SKTexture textureWithImageNamed:] or using [SKTextureAtlas textureNamed:].

      • Another example: Let’s say you create a texture atlas object using [SKTextureAtlas atlasNamed:]. You take one of its textures using textureNamed:, and you don’t retain the atlas. You display the texture in a sprite node (and so the texture is retained strongly in your app), but you don’t bother tracking that particular SKTexture in a cache. Then you do all of that over again: new texture atlas, new texture, new node. All of these objects will be freshly allocated, but they are relatively lightweight. Meanwhile, and importantly: the bulky image data originally loaded will be transparently shared between instances.

      • Try this one: You load a monsters atlas by name, and then take one of its orc textures and render it in an orc sprite node. Then, the player returns to home screen. You encode the orc node during application state preservation, and then decode it during application state restoration. (When it encodes, it doesn’t encode its binary data; it encodes its name instead.) In the restored app, you create another orc (with new atlas, texture, and node). Will this new orc share its bulky orc data with the decoded orc? Yes. Yes it orcking will.

      • Pretty much the only way to get a texture not to reuse texture image data is to initialize it using [SKTexture textureWithImage:]. Sure, maybe UIImage will do its own internal caching of the image file, but either way, SKTexture takes charge of the data, and will not reuse the render data elsewhere.

      • In short: If you’ve got two of the same sprite showing in your game at the same time, it’s a fair bet they are using memory efficiently.

    Put those two points together: SpriteKit has a built-in cache that persists the important bulky image data and reuses it smartly.

    In other words, it just works.

    No promises. In the simulator running a test app, I can easily prove that SpriteKit is deleting my texture data from cache before I’m really done with it.

    During prototyping, though, you might be surprised to find reasonably good behavior from your app even if you never reuse a single atlas or texture.

    SpriteKit Has Atlas Caching Too

    SpriteKit has a caching mechanism specifically for texture atlases. It works like this:

    Texture objects, it should be mentioned, do not retain their atlases.

    You Might Still Want To Build Your Own Cache

    So I see you are building your own caching and reuse mechanisms anyway. Why are you doing it?

    All that said, your caching and reuse behavior will probably end up eerily similar to SpriteKit’s.

    How does SpriteKit’s texture caching affect your own texture cache design? Here are the things (from above) to keep in mind:

    Testing

    I’m a tools novice, so I just measured overall app memory usage on a release build using the Allocations instrument.

    I found that “All Heap & Anonymous VM” would alternate between two stable values on sequential runs. I ran each test a few times and used the lowest memory value as the result.

    For my testing I’ve got two different atlases with two images each; call the atlases A and B and the images 1 and 2. The source images are largish (one 760 KiB, one 950 KiB).

    Atlases are loaded using [SKTextureAtlas atlasNamed:]. Textures are loaded using [SKTexture textureWithImageNamed:]. In the table below, load really means “putting in a sprite node and rendering.”

     All Heap
    & Anon VM
        (MiB)  Test
    ---------  ------------------------------------------------------
    
       106.67  baseline
       106.67  preload atlases but no nodes
    
       110.81  load A1
       110.81  load A1 and reuse in different two sprite nodes
       110.81  load A1 with retained atlas
       110.81  load A1,A1
       110.81  load A1,A1 with retained atlas
       110.81  load A1,A2
       110.81  load A1,A2 with retained atlas
       110.81  load A1 two different ways*
       110.81  load A1 two different ways* with retained atlas
       110.81  load A1 or A2 randomly on each tap
       110.81  load A1 or A2 randomly on each tap with retained atlas
    
       114.87  load A1,B1
       114.87  load A1,A2,B1,B2
       114.87  load A1,A2,B1,B2 with preload atlases
    
    * Load A1 two different ways: Once using [SKTexture
      textureWithImageNamed:] and once using [SKTextureAtlas
      textureNamed:].
    

    Internal Structure

    While investigating I discovered some true facts about the internal structure of texture and atlas objects in SpriteKit.

    Interesting? That depends on what kinds of things interest you!

    Structure of a Texture From an SKTextureAtlas

    When an atlas is loaded by [SKTextureAtlas atlasNamed:], inspection of its textures at runtime shows some reuse.

    If the atlas is released from memory and then reloaded, the new copy will have different SKTexture objects, different _originalTexture objects, and different _textureCache objects. From what I could see, only the _imgName (that is, the actual image file) connects the new atlas to the old atlas.

    Structure of a Texture Not From an SKTextureAtlas

    When a texture is loaded using [SKTexture textureWithImageNamed:], it may come from an atlas, but it doesn’t seem to come from an SKTextureAtlas.

    A texture loaded this way has differences from the above:

    Two SKTexture objects loaded by textureWithImageNamed: (with the same image name) have nothing notable in common other than _imgName.

    Nevertheless, as thoroughly belabored above, this kind of texture configuration shares bulky image data with the other kind of texture configuration. This implies that caching is done close to the actual image file.