SKTexture 缓存和重用在 SpriteKit 中是如何工作的?

How does SKTexture caching and reuse work in SpriteKit?

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

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

SpriteKit 中的纹理缓存

“长话短说:依靠 Sprite Kit 为 你。” -@LearnCocos2D

长话短说,截至 iOS 9.

纹理庞大的图像数据没有直接保存在SKTexture 目的。 (参见 SKTexture class 参考.)

  • SKTexture 延迟加载数据直到必要。创建一个 纹理,即使来自大图像文件,也很快并且消耗很少 内存.

  • 一个纹理的数据通常是从磁盘加载的 精灵节点被创建。 (或者实际上,每当需要数据时, 例如当 size 方法被调用时)。

  • 纹理数据已准备好在(第一个)期间进行渲染 渲染过程。

SpriteKit 有一些内置的缓存用于纹理的大图像 数据。两个特点:

  1. 纹理的庞大图像数据被SpriteKit缓存直到 SpriteKit 感觉要摆脱它了

    • 根据 SKTexture class 参考:“一旦 SKTexture 对象已准备好渲染,它会一直准备好直到一切都准备好 对纹理对象的引用被删除。”

    • 在当前 iOS,它往往会停留更长时间,甚至可能 整个场景消失后。 计算器溢出 comment 引用苹果 技术支持:“iOS 释放缓存的内存 +textureWithImageNamed: 或 +imageNamed: 当它认为合适时,对于 例如,当它检测到内存不足的情况时。”

    • 在模拟器中,运行一个测试项目,我是可以看到的 removeFromParent 后立即回收纹理内存。 运行 但是,在物理设备上,内存似乎 萦绕;重复渲染和释放纹理 导致没有额外的磁盘访问。

    • 我想知道:渲染内存在某些情况下是否可以提前释放 内存临界情况(当纹理被保留但不 当前显示)?

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

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

    • 假设你有一个纹理显示在精灵节点中,而是 与重用 SKTexture 对象相比,您可以为相同的图像名称调用 [SKTexture textureWithImageNamed:]。质地 不会与原始纹理指​​针相同,但它 将共享庞大的图像数据。

    • 无论图像文件是图集的一部分还是 不是。

    • 如果原始贴图是使用 [SKTexture textureWithImageNamed:] 或使用 [SKTextureAtlas textureNamed:].

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

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

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

    • 简而言之:如果你有两个相同的精灵出现在你的 同时玩游戏,可以肯定他们正在使用内存 高效。

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

换句话说,它就是有效。

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

不过,在原型制作过程中,您可能会惊讶地发现合理的 即使您从不重复使用单个地图集或 纹理。

SpriteKit 也有图集缓存

SpriteKit 有一个专门针对纹理图集的缓存机制。 它是这样工作的:

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

  • 您在应用中的某处强烈保留了地图集。

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

纹理对象,需要说明的是,不要保留它们的图集。

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

所以我看到您正在构建自己的缓存和重用机制 反正。你为什么要这样做?

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

  • 您可能需要完全控制加载时间。为了 例如,如果你想让你的纹理在第一次出现时立即出现 呈现后,您将使用 SKTexture 中的 preload 方法和 SKTextureAtlas。在这种情况下,您应该保留参考文献 到预加载的纹理或地图集,对吧?或者,SpriteKit 会 无论如何都为你缓存它们?不清楚。自定义图集或纹理 缓存是保持完全控制的好方法。

  • 在某个优化点上(天禁过早!!), 停止创建新的 SKTexture and/or 是有意义的 SKTextureAtlas 对象一遍又一遍,无论多么轻便。 可能你会首先构建 atlas-reuse 机制,因为 atlases 不那么轻巧(他们有一个纹理字典,之后 全部)。稍后您可能会构建一个单独的纹理缓存机制 用于重用非图集 SKTexture 对象。或者也许你永远不会 转到第二个。毕竟你很忙,而且 厨房不会自己打扫,该死。

综上所述,您的缓存和重用行为可能会结束 与 SpriteKit 惊人地相似。

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

  • 您无法直接控制大图的发布时间 使用命名纹理时的数据。您发布您的参考资料,并且 SpriteKit 在需要时释放内存。

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

  • 如果你依赖SpriteKit的内部缓存,那么你的图集缓存 只需要 保留 SKTextureAtlas 对象的引用, 不是 return 他们。 atlas 对象将自动被重用 整个应用程序。

  • 同样,你的纹理缓存只需要保留SKTexture 个对象,而不是 return 个对象。庞大的图像数据 将在您的整个应用程序中自动重用。 (这个怪怪的 不过,我有点出局;验证良好的行为是一件痛苦的事。)

  • 鉴于最后两点,考虑设计备选方案 单例缓存对象。相反,您可以保留正在使用的地图集 在您的精灵对象或其控制器上。对于的一生 控制器,然后,您应用中对 atlasNamed: 的任何调用都会 重用指针相同的图集。

  • 两个指针相同的SKTexture对象共享同一内存, 是的,但由于 SpriteKit 缓存,反过来不一定 真的。如果您正在调试内存问题并发现两个 SKTexture 您期望指针相同但实际上不是的对象 仍然可能会共享他们庞大的图像数据。

测试

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

我发现“All Heap & Anonymous VM”会在两个之间交替 stable 顺序运行的值。我运行每次测试几次 使用最低内存值作为结果。

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

使用 [SKTextureAtlas atlasNamed:] 加载地图集。纹理是 使用 [SKTexture textureWithImageNamed:] 加载。在 table 下面,load 真正的意思是“放入精灵节点并渲染”。

 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构建期间,一个脚本从个人编译图集 图像文件转换为多个大精灵 sheet 图像(受限于 大小,并按@1x @2x @3x 分辨率分组)。里面的每一个纹路 atlas 通过 bundle 路径引用它的 sprite sheet 图像,存储在 _imgName_isPath 设置为真)。

  • 图集中的每个纹理都由其单独标识 _subTextureName,并在其 sprite 中插入了 textureRect sheet.

  • 图集中共享相同精灵的所有纹理sheet图像 将具有相同的非零 ivars _originalTexture_textureCache.

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

如果图集从内存中释放然后重新加载,新的副本 将有不同的 SKTexture 个对象,不同的 _originalTexture 对象,以及不同的 _textureCache 个对象。尽我所能 看,只有_imgName(也就是实际的图像文件)连接了 旧图集的新图集。

不是来自 SKTextureAtlas

的纹理结构

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

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

  • 它有一个短的_imgName,如“giraffe.png”,并且设置了_isPath 假的。

  • 它有一个未设置的 _originalTexture

  • 它(显然)有自己的 _textureCache

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

之外没有任何共同点table

不过,正如上面详细说明的那样,这种质地 配置与另一种纹理共享庞大的图像数据 配置。这意味着缓存是接近实际的 图像文件。