How does SKTexture caching and reuse work in SpriteKit?

心不动则不痛 提交于 2019-11-28 20:53:21
Karl Voskuil

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.)

  • SKTexture defers loading data until necessary. Creating a texture, even from a large image file, is quick and consumes little memory.

  • Data for a texture is loaded from disk usually when a corresponding sprite node is created. (Or really, whenever the data is needed, for example when the size method is called).

  • Data for a texture is prepared for rendering during the (first) render pass.

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:

  • You call [SKTextureAtlas atlasNamed:] to load a texture atlas. (As mentioned before, this doesn’t yet load the bulky image data.)

  • You retain the atlas strongly somewhere in your app.

  • Later, if you call [SKTextureAtlas atlasNamed:] with the same atlas name, the object returned will be pointer-identical to the retained atlas. Textures extracted from the atlas using textureNamed:, then, will be pointer-identical, too. (Update: The textures will not necessarily be pointer-identical under iOS10.)

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?

  • Ultimately you have better information about when to keep or purge certain textures.

  • You might need complete control over the load timing. For instance, if you want your textures to appear instantly when first rendered, you’ll use the preload methods from SKTexture and SKTextureAtlas. In that case, you should retain the references to the preloaded textures or atlases, right? Or, will SpriteKit cache them for you regardless? Unclear. A custom atlas or texture cache is a good way to keep complete control.

  • At a certain point of optimization (heaven forbid prematurely!!), it makes sense to stop creating new SKTexture and/or SKTextureAtlas objects over and over, no matter how lightweight. Probably you’d build the atlas-reuse mechanism first, since atlases are less lightweight (they have a dictionary of textures, after all). Later you might build a separate texture caching mechanism for reuse of non-atlas SKTexture objects. Or maybe you’d never get around to that second one. You are busy, after all, and the kitchen isn’t cleaning itself, dammit.

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:

  • You can’t directly control the timing of the release of bulky image data when using named textures. You release your references, and SpriteKit releases the memory when it wants to.

  • You can control the timing of the loading of bulky image data, using the preload methods.

  • If you rely on SpriteKit’s internal caching, then your atlas cache needs only retain references to the SKTextureAtlas objects, not return them. The atlas object will automatically be reused throughout your app.

  • Similarly, your texture cache needs only retain references to the SKTexture objects, not return them. The bulky image data will be automatically reused throughout your app. (This one weirds me out a bit, though; it’s a pain to verify good behavior.)

  • Given the last two points, consider design alternatives to a singleton cache object. Instead, you could retain in-use atlases on your sprite objects or their controllers. For the lifetime of the controller, then, any calls in your app to atlasNamed: will reuse the pointer-identical atlas.

  • Two pointer-identical SKTexture objects share the same memory, yes, but due to SpriteKit caching, the converse isn’t necessarily true. If you’re debugging memory problems and find two SKTexture objects that you expected to be pointer-identical, but aren’t, they still might be sharing their bulky image data.

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.

  • During Xcode build, a script compiles the atlas from individual image files into a number of large sprite sheet images (limited by size, and grouped by @1x @2x @3x resolution). Each texture in the atlas refers to its sprite sheet image by bundle path, stored in _imgName (with _isPath set true).

  • Each texture in the atlas is individually identified by its _subTextureName, and has a textureRect inset into its sprite sheet.

  • All textures in the atlas that share the same sprite sheet image will have identical non-nil ivars _originalTexture and _textureCache.

  • The shared _originalTexture, itself an SKTexture object, presumably represents the whole sprite sheet image. It has no _subTextureName of its own, and its textureRect is (0, 0, 1, 1).

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:

  • It has a short _imgName, like “giraffe.png”, and _isPath is set false.

  • It has an unset _originalTexture.

  • It has (apparently) its own _textureCache.

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!