资源释放

文:Santy-Wang

Asset Manager 中提供了资源释放模块,用于管理资源的释放,自动释放等功能。

释放资源

当资源被加载后,将会被保存在缓存中,供下次复用,而这会造成内存和显存持续增长,所以你需要做的是在不需要该资源时,对其进行释放,从而将其移出缓存,释放内存和显存(对纹理而言)。Creator 提供了自动释放与手动释放两者形式:

自动释放

Creator v2.4 提供了自动释放机制,首先,场景提供了自动释放选项,如图所示:

自动释放

勾选之后,则会在该场景切换时尝试自动释放该场景所有依赖资源。

除非是某些高频使用的场景,比如主场景,否则我们建议你尽量全部勾选自动释放选项,以确保内存占用较低。

另外引擎提供了引用计数的统计函数 cc.Asset.addRef 以及 cc.Asset.decRef 分别用于增加和减少引用计数。当你调用 decRef 后,Creator 也会尝试对其进行自动释放。

  1. start () {
  2. cc.resources.load('images/background', cc.Texture2D, (err, texture) => {
  3. this.texture = texture;
  4. // 当需要持有资源时,增加其引用
  5. texture.addRef();
  6. // ...
  7. });
  8. }
  9. onDestroy () {
  10. // 当你不需要持有资源时,减少其引用,Creator 会在调用 decRef 尝试对其进行自动释放
  11. this.texture.decRef();
  12. }

自动释放的优势在于不用显式地调用释放接口,你只需维护好资源的引用计数,Creator 会根据引用计数自动进行释放。这大大降低了错误释放资源的可能性,并且你不需要了解资源之间复杂的引用关系,推荐在没有特殊需求的项目中尽量使用自动释放形式。

释放检查

为了避免错误释放正在使用的资源造成渲染或其他问题,在 Creator 尝试进行自动释放资源时,会进行一系列的检查,只有通过检查,才会对该资源进行释放。检查步骤如下:

  1. 如果该资源的引用计数为 0,即没有其他地方引用到该资源,则无需做后续检查,直接摧毁该资源,移除缓存,并将其 直接 依赖资源(不包含后代)的引用都减 1,并触发依赖资源的释放检查。

  2. 如果该资源的引用计数不为 0,存在其他地方引用它,此时需要进行循环引用检查,避免自己的后代引用自己的情况。如果循环引用检查之后引用计数仍不为 0,则终止释放,否则直接摧毁该资源,移除缓存,并将其 直接 依赖资源(不包含后代)的引用都减 1,并触发依赖资源的释放检查。

经过上述检查后,如果该资源可释放,则将会摧毁该资源,并移除缓存,并触发其依赖资源的释放检查。

手动释放

除了自动释放外,Creator 同样提供了手动释放接口,当项目有更复杂的资源释放机制时,可以调用 Asset Manager 相关接口手动释放资源。

例如,你可以如下使用:

  1. cc.assetManager.releaseAsset(texture);

释放该资源将会销毁该资源的所有内部属性,比如渲染层的相关数据,并移出缓存,从而释放内存和显存(对纹理而言)。

v2.4 中的释放接口与之前版本的释放接口类似,区别有以下几点:

  1. cc.assetManager.releaseAsset 接口仅能释放单个资源,且为了统一,接口只能通过资源本身进行释放,不能通过资源 uuid,资源 url 等属性进行释放。

  2. 为了方便开发者使用,在开发者释放资源时不再需要通过 getDependsRecursively 获取依赖资源。你只需关注资源本身,而引擎会去尝试 自动释放 其依赖资源。

注意:通过 releaseAsset 接口释放的资源本身不会进行释放检查,只有其依赖资源会进行释放检查。所以 releasereleaseAsset 接口能保证资源本身一定被释放。

引用计数统计

首先需要声明的是,Asset Manager 中的引用计数统计与传统 C++ 语言实现的引用计数不同。Asset Manager 只会自动统计资源之间的静态引用,并不能真实的反应资源在游戏中被动态持有的情况。原因在于:

  1. js 是拥有垃圾回收机制的语言,其会对其内存进行管理,上层无法知道某个资源是否被销毁,是否被引用。

  2. js 是动态类型语言,其无法提供赋值运算符的重载,而引用计数的统计是高度依赖于赋值运算符的重载的。

所以,因为这两个问题,在 v2.4 之前,Creator 很长时间里选择让开发者控制所有资源的释放,包括资源本身和它的依赖项,你必须手动获取资源所有的依赖项并选择需要释放的依赖项,这种方式给予了开发者最大的控制权力,对于小型项目来说工作良好,但随着 Creator 的发展,项目的规模不断提升,场景所引用的资源不断增加,而其他场景可能也复用了这些资源,这会造成释放资源的复杂度越来越高,开发者需要掌握所有资源的使用非常困难。为了解决这个痛点,Asset Manager 提供了一套基于引用计数的资源释放机制,让开发者可以简单高效地释放资源,不用担心项目规模的急剧膨胀。需要说明的是这套方案中引擎仅对资源的静态引用做了准确的计数,但资源的动态引用还需要开发者进行控制以保证资源能够被正确释放。

首先需要先说明什么是资源的静态引用和动态引用:

  1. 当你在编辑器中编辑资源时,例如场景,预制体,材质时,此时你会将一些其他资源设置到他们的属性上,例如设置贴图到材质中,将 SpriteFrame 设置到场景中的 Sprite 组件上,此时这些引用关系将会记录在资源的序列化数据中,引擎可以通过这些数据分析出依赖资源列表,那它们之间的引用关系就是静态的。

  2. 第二种情况是,当你在编辑器中编辑资源时没有设置任何属性,而在游戏运行时,在代码中动态加载资源并设置到场景中组件上时,此时的引用关系没有存在序列化数据中,所以引擎无法统计到这部分的引用关系,这部分叫做资源的动态引用。

引擎对资源的静态引用的统计方式为:

  1. 在使用 cc.assetManager 或 Asset Bundle 加载某个资源时,在底层加载管线中,记录该资源的所有 直接 依赖资源信息,并将所有 直接 依赖资源的计数加 1,并且初始化该资源的引用计数为 0。

  2. 在释放资源时,取得该资源之前记录的 直接 依赖资源信息,将所有依赖资源的计数减 1。

因为在释放检查时,需要检查该资源的引用计数是否为 0,所以上述步骤可以保证子资源无法先于父资源被释放,因为其计数肯定不为 0。也就是说,只要一个资源本身不被释放,其依赖的资源就不会被释放,从而保证在复用资源时不会错误地进行释放。下面举例说明:

  1. 现在存在 A 预制体,其依赖两个资源:a 材质,b 材质,a 材质引用了贴图 α,b 材质引用了贴图 β。则在加载 A 预制体之后,则 a,b 材质的计数都为 1,贴图 α,β 的计数也都为 1。

  2. 现在有一个 B 预制体,也依赖两个资源:b 材质,c 材质。则在加载之后,b 材质的计数为 2,因为其同时被两个预制体所引用,c 材质的计数为 1,贴图 α,β 的计数还是为 1。

  3. 此时,释放 A 预制体,则 a,b 材质的计数各减 1。a 的计数变为了 0,a 被释放,则贴图 α 的计数减 1,贴图 α 的计数变为了 0,贴图 α 被释放,而 b 材质的计数为 1,则 b 被保留,贴图 β 的计数为 1,被保留。

上面例子说明,在资源复用时,能够保证复用的资源不会被错误释放。

如果你的工程中使用了动态加载资源来进行动态引用,例如:

  1. cc.resources.load('images/background', cc.SpriteFrame, function (err, spriteFrame) {
  2. self.getComponent(cc.Sprite).spriteFrame = spriteFrame;
  3. });

此时这个资源虽然设置给 Sprite 组件,但它的引用计数将保持默认为 0,引擎不会进行特殊处理。如果你动态加载出来的资源需要进行长期引用、持有,或者复用时,建议你使用 addRef 接口手动增加引用计数。例如:

  1. cc.resources.load('images/background', cc.SpriteFrame, function (err, spriteFrame) {
  2. self.getComponent(cc.Sprite).spriteFrame = spriteFrame;
  3. spriteFrame.addRef();
  4. });

增加引用计数后,能够保证该资源不会被提前错误释放掉。而在不需要引用该资源时,请 务必记住 使用 decRef 移除引用计数,并将资源引用设为 null,例如:

  1. this.spriteFrame.decRef();
  2. this.spriteFrame = null;