10

在 StackOverflow 上经常提到 SpriteKit 做自己的内部缓存和重用。

如果我不努力重用纹理或图集,我可以从 SpriteKit 获得什么缓存和重用行为?

4

1 回答 1

22

SpriteKit 中的纹理缓存

“长话短说:依靠 Sprite Kit 为您做正确的事。”</a> -@LearnCocos2D

从 iOS 9 开始,长话短说。

纹理的庞大图像数据不直接保存在SKTexture 对象上。(参见SKTexture类参考。)

  • SKTexture延迟加载数据,直到需要。即使从大图像文件创建纹理也很快,并且占用的内存很少。

  • 纹理的数据通常在创建相应的精灵节点时从磁盘加载。(或者实际上,只要需要数据,例如size调用方法时)。

  • 在(第一次)渲染过程中为渲染准备纹理数据。

SpriteKit 为纹理的庞大图像数据提供了一些内置缓存。两个特点:

  1. 纹理的庞大图像数据由 SpriteKit 缓存,直到 SpriteKit 想摆脱它。

    • 根据SKTexture类引用:“一旦 SKTexture 对象准备好进行渲染,它就会一直准备就绪,直到对纹理对象的所有强引用都被删除。”</p>

    • 在当前的 iOS 中,它往往会停留更长时间,甚至可能在整个场景消失之后。 StackOverflow 的一条评论引用了 Apple 技术支持:“iOS 会在它认为合适时释放由 +textureWithImageNamed: 或 +imageNamed: 缓存的内存,例如当它检测到内存不足的情况时。”</p>

    • 在模拟器中,运行一个测试项目,我能够看到纹理内存在removeFromParent. 但是,在物理设备上运行时,内存似乎会流连忘返。重复渲染和释放纹理不会导致额外的磁盘访问。

    • 我想知道:在某些内存关键的情况下(当纹理保留但当前未显示时),是否可以提前释放渲染内存?

  2. SpriteKit 巧妙地重用缓存的大容量图像数据。

    • 在我的实验中,很难重复使用它。

    • 假设您在 sprite 节点中显示了纹理,但不是重用该SKTexture对象,而是调用[SKTexture textureWithImageNamed:]相同的图像名称。纹理不会与原始纹理指​​针相同,但会共享庞大的图像数据。

    • 无论图像文件是否是图集的一部分,上述情况都是正确的。

    • [SKTexture textureWithImageNamed:]无论原始纹理是使用加载还是使用加载,上述情况都是正确的 [SKTextureAtlas textureNamed:]

    • 另一个例子:假设您使用[SKTextureAtlas atlasNamed:]. 您使用 获取其纹理之一textureNamed:,并且不保留图集。您在 sprite 节点中显示纹理(因此纹理在您的应用程序中被强烈保留),但您不必费心SKTexture在缓存中跟踪该特定内容。然后你再做一遍:新纹理图集、新纹理、新节点。所有这些对象都将被新分配,但它们相对较轻。同时,重要的是:最初加载的庞大图像数据将在实例之间透明地共享。

    • 试试这个:您按名称加载怪物图集,然后获取其中一个兽人纹理并将其渲染到兽人精灵节点中。然后,播放器返回主屏幕。您在应用程序状态保存期间对 orc 节点进行编码,然后在应用程序状态恢复期间对其进行解码。(当它编码时,它不会对其二进制数据进行编码;而是对其名称进行编码。)在恢复的应用程序中,您创建另一个兽人(具有新的图集、纹理和节点)。这个新的兽人会与解码的兽人共享其庞大的兽人数据吗?是的。是的,它会发疯。

    • 获得纹理而不重用纹理图像数据的唯一方法几乎是使用[SKTexture textureWithImage:]. 当然,也许UIImage会自己对图像文件进行内部缓存,但无论哪种方式,SKTexture 都会负责数据,并且不会在其他地方重用渲染数据。

    • 简而言之:如果您的游戏中同时显示了两个相同的精灵,那么可以肯定他们正在有效地使用内存。

将这两点放在一起:SpriteKit 有一个内置的缓存,可以保存重要的大容量图像数据并巧妙地重用它。

换句话说,它只是工作。

没有承诺。在运行测试应用程序的模拟器中,我可以很容易地证明 SpriteKit 在我真正完成之前正在从缓存中删除我的纹理数据。

但是,在原型设计期间,即使您从未重复使用单个图集或纹理,您也可能会惊讶地发现应用程序的行为相当不错。

SpriteKit 也有 Atlas 缓存

SpriteKit 具有专门用于纹理图集的缓存机制。它是这样工作的:

  • 您调用[SKTextureAtlas atlasNamed:]以加载纹理图集。(如前所述,这还没有加载庞大的图像数据。)

  • 您可以在应用程序的某个位置强烈保留地图集。

  • 稍后,如果您[SKTextureAtlas atlasNamed:]使用相同的图集名称调用,则返回的对象将与保留的图集指针相同。textureNamed:那么,使用从图集中提取的纹理 也将是指针相同的。(更新:纹理在 iOS10 下不一定是指针相同的。)

应该提到的是,纹理对象不保留它们的图集。

您可能仍想建立自己的缓存

所以我看到你无论如何都在构建自己的缓存和重用机制。你为什么这样做?

  • 最终,您可以获得有关何时保留或清除某些纹理的更好信息。

  • 您可能需要完全控制加载时间。例如,如果您希望纹理在第一次渲染时立即出现,您将使用preload方法 fromSKTextureSKTextureAtlas。在这种情况下,您应该保留对预加载纹理或图集的引用,对吗?或者,SpriteKit 会为您缓存它们吗?不清楚。自定义图集或纹理缓存是保持完全控制的好方法。

  • 在某个优化点(天堂禁止过早!!SKTexture ),停止一遍又一遍地创建新对象和/或 对象是有意义的SKTextureAtlas,无论多么轻量级。可能您会首先构建图集重用机制,因为图集不那么轻量级(毕竟它们有一个纹理字典)。稍后您可能会构建一个单独的纹理缓存机制来重用非图集SKTexture对象。或者,也许你永远也无法解决第二个问题。毕竟你很忙,而且厨房不会自己打扫,该死。

话虽如此,您的缓存和重用行为最终可能会与 SpriteKit 的惊人相似。

SpriteKit 的纹理缓存如何影响您自己的纹理缓存设计?以下是要记住的事情(从上面):

  • 使用命名纹理时,您无法直接控制释放大量图像数据的时间。你释放你的引用,SpriteKit 在它想要的时候释放内存。

  • 可以preload使用这些方法控制大容量图像数据的加载时间。

  • 如果你依赖 SpriteKit 的内部缓存,那么你的 atlas 缓存只需要保留SKTextureAtlas对象的引用,而不是返回它们。atlas 对象将在整个应用程序中自动重用。

  • 同样,您的纹理缓存只需要保留对对象的引用SKTexture,而不需要返回它们。庞大的图像数据将在整个应用程序中自动重复使用。(不过,这让我有点奇怪;验证良好行为是一种痛苦。)

  • 鉴于最后两点,请考虑单例缓存对象的设计替代方案。相反,您可以在精灵对象或其控制器上保留使用中的图集。那么,对于控制器的生命周期,您的应用程序中的任何调用都atlasNamed:将重用指针相同的图集。

  • 两个指针相同SKTexture的对象共享相同的内存,是的,但是由于 SpriteKit 缓存,反过来不一定正确。如果您正在调试内存问题并找到两个SKTexture 您希望指针相同但实际上并非如此的对象,那么它们可能仍然在共享其庞大的图像数据。

测试

我是一个工具新手,所以我只是使用 Allocations 工具测量了发布版本的整体应用程序内存使用情况。

我发现“All Heap & Anonymous VM”会在连续运行时在两个稳定值之间交替。我对每个测试运行了几次,并使用最低的内存值作为结果。

对于我的测试,我有两个不同的图集,每个图集有两个图像;调用图集 A 和 B 以及图像 1 和 2。源图像较大(一个 760 KiB,一个 950 KiB)。

地图集使用[SKTextureAtlas atlasNamed:]. 纹理使用[SKTexture textureWithImageNamed:]. 在下表中,load的真正含义是“放入一个 sprite 节点并进行渲染”。</p>

 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:].

内部结构

在调查过程中,我发现了一些关于 SpriteKit 中纹理和图集对象内部结构的真实事实。

有趣的?这取决于您对哪种事物感兴趣!

纹理的结构SKTextureAtlas

当一个图集由 加载时[SKTextureAtlas atlasNamed:],在运行时检查它的纹理会显示一些重用。

  • 在 Xcode 构建期间,一个脚本将图集从单个图像文件编译成许多大型精灵表图像(受大小限制,并按@1x @2x @3x 分辨率分组)。地图集中的每个纹理通过捆绑路径引用其精灵表图像,存储在 _imgName_isPath设置为true)中。

  • 地图集中的每个纹理都由其单独标识 _subTextureName,并textureRect在其 sprite 表中插入一个插图。

  • atlas 中共享相同 sprite sheet 图像的所有纹理将具有相同的 non-nil ivars_originalTexture_textureCache.

  • shared_originalTexture本身就是一个SKTexture对象,大概代表了整个精灵表图像。它没有 _subTextureName它自己的,它textureRect(0, 0, 1, 1)

如果图集从内存中释放然后重新加载,新的副本会有不同的SKTexture对象,不同的_originalTexture 对象,不同的_textureCache对象。据我所知,只有_imgName(即实际的图像文件)将新图集连接到旧图集。

纹理的结构不是来自SKTextureAtlas

当使用 加载纹理时[SKTexture textureWithImageNamed:],它可能来自图集,但似乎不是来自 SKTextureAtlas.

以这种方式加载的纹理与上述不同:

  • 它有一个简短的_imgName,如“giraffe.png”,并_isPath设置为 false。

  • 它有一个未设置的_originalTexture.

  • 它(显然)有自己的_textureCache.

SKTexture加载的两个对象textureWithImageNamed:(具有相同的图像名称)除了_imgName.

然而,正如上面详细阐述的那样,这种纹理配置与另一种纹理配置共享大量图像数据。这意味着缓存是在接近实际图像文件的地方完成的。

于 2016-05-09T15:31:31.040 回答