cocosCreator 之内存管理和释放

news2024/12/26 11:01:46

版本: 3.4.0

语言: TypeScript

环境: Mac


回顾


前面有两篇博客说明了:

  • cocosCreator 之 resources动态加载、预加载 讲述了静态引用资源,动态加载和预加载相关
  • cocosCreator 之 Bundle 讲述了AssetManager关于对内置Bundle和自定义Bundle的使用相关

简单的理解就是对cocosCreator内静态动态引用资源的使用相关,为了对动态资源更方便管理,增加了AssetManager用于管理,释放资源相关。

动态引用的资源,相关接口均为异步操作

涉及到资源管理,就会牵扯到资源的内存管理。

在cocosCreator中,官方针对于不同的资源有着不同的内存管理方式。主要有:

  • 静态引用资源,通过序列化数据进行自动管理释放
  • 动态引用资源,为了避免错误释放而增加引用计数管理, 以及AssetManager对资源进行的释放管理
  • 场景的自动释放管理

从本质上都是引用计数,但为了有一个更好的理解,故此通过本篇博客汇总出来。

理解可能有误,欢迎您的指出。


引用计数


cocosCreator中的资源都被放在 assets目录下, 主要来源:

  • 从外部导入
  • 通过远程下载的资源

他们最后都会被包装,使其继承于资源基类:Asset

对象
事件处理
资源基类
cocos_core_assets_asset_Asset_base
CCObject
cocos_core_event_eventify_IEventified
Asset

在cocosCreator中,Asset的重要作用就是对资源进行引用计数。主要定义如下:

// cc.d.ts
export class Asset extends __private.cocos_core_assets_asset_Asset_base {
  // 该资源对应的目标平台资源的 URL,如果没有将返回一个空字符串
  get nativeUrl(): string;
  // 序列化对象
  serialize(): void;
  // 获取引用数量
  get refCount(): number;
  // 增加引用计数
  addRef(): Asset;
  // 减少资源的引用并尝试进行自动释放
  decRef(autoRelease?: boolean): Asset;
}

// 主要实现: ../resources/3d/engine/cocos/core/assets/asset.ts
export class Asset extends Eventify(CCObject) {
  private _ref = 0;
  // 引用计数数目
  public get refCount (): number {
    return this._ref;
  }
	// 引用计数+1
  public addRef (): Asset {
    this._ref++;
    return this;
  }
	// 引用计数-1,并尝试进行自动释放
  public decRef (autoRelease = true): Asset {
    if (this._ref > 0) {
      this._ref--;
    }
    // 检测是否自动释放
    if (autoRelease) {
      legacyCC.assetManager._releaseManager.tryRelease(this);
    }
    return this;
  }
}

针对于decRef下的自动释放接口 tryRelease, 我们看下大致的实现:

// ../resources/3d/engine/cocos/core/asset-manager/release-manager.ts
class ReleaseManager {
  private _eventListener = false;
  // 待释放资源数组
  private _toDelete = new Cache<Asset>();
  
  // 尝试自动释放(释放对象,是否强制释放默认为false)
  public tryRelease (asset: Asset, force = false): void {
    if (!(asset instanceof Asset)) { return; }
    // 如果强制释放,则释放资源
    if (force) {
      this._free(asset, force);
      return;
    }
		// 没有强制释放,则将对象的uuid缓存到待释放资源对象中
    this._toDelete.add(asset._uuid, asset);
    // 检测对象是否注册事件监听器,如果没注册,则下一帧进行释放资检测
    if (!this._eventListener) {
      this._eventListener = true;
      callInNextTick(this._freeAssets.bind(this));
    }
  }
  
  // 用于事件监听器的下一帧释放检测
  private _freeAssets () {
    this._eventListener = false;
    this._toDelete.forEach((asset) => {
      this._free(asset);
    });
    // 注意:清空用于保证缓存的对象仅被遍历一次,也就是生命周期仅有一帧
    this._toDelete.clear();
  }
  
	// 释放对象(对象,是否强制释放)
  private _free (asset: Asset, force = false) {
    const uuid = asset._uuid;
    // 将释放对象从缓存中移除
    this._toDelete.remove(uuid);
		// 检测对象是否有效
    if (!isValid(asset, true)) { return; }
		
    if (!force) {
      // 检测引用计数和是否存在循环引用,如果存在则return
      if (asset.refCount > 0) {
        if (checkCircularReference(asset) > 0) { return; }
      }
    }

    // 从缓存中移除对象
    assets.remove(uuid);
    // 通过uuid获取资源的所有依赖项,并进行遍历
    const depends = dependUtil.getDeps(uuid);
    for (let i = 0, l = depends.length; i < l; i++) {
      // 对象有效,则进行引用计数-1
      const dependAsset = assets.get(depends[i]);
      if (dependAsset) {
        dependAsset.decRef(false);
        // no need to release dependencies recursively in editor
        if (!EDITOR) {
          this._free(dependAsset, false);
        }
      }
    }
    // ...
  }
}

它的流程简介:

  • 如果不是强制释放对象,则存储到临时数组中,在下一帧遍历缓存中数组对象进行释放操作
  • 如果是强制释放对象,则调用释放接口
  • 释放接口会将对象从临时数组中移除,并检测对象是否有效、是否被引用
  • 如果对象可以被移除,则获取依赖项并进行遍历进行引用计数-1
  • 引用计数为0,则对对象进行释放。

这里有几点需要注意:

  1. 针对于this._eventListener 是一个标记,它主要用于保证对象需要在下一帧执行
  2. 释放操作中的对象增加操作this._toDelete.clear(),主要是为了保证对象的生命周期只有一帧

针对于后者,生命周期回调仅有一帧,很像cocos2d-x中的内存管理处理:

// application.cpp的while主循环中,根据FPS每帧调用mainLoop
void Director::mainLoop() {
  if (! _invalid) {
    drawScene();
    // 清理当前释放池对象
    PoolManager::getInstance()->getCurrentPool()->clear();
  }
}

void AutoreleasePool::clear() {
  // 通过使用vector.swap方法进行交换,可以保证每帧仅对节点数据遍历一次
  std::vector<Ref*> releasings;
  releasings.swap(_managedObjectArray);

  // 遍历所有对象,进行引用计数-1,为0的销毁对象
  for (const auto &obj : releasings) {
    obj->release();
  }
}

关于cocos2d-x的内存机制可参考:cocos2d-x 内存管理机制

cocosCreator中的资源很多都是相互依赖的,他们的引用计数结构类似如下:

  1. 当使用到某个资源时,引用计数是:
    请添加图片描述

  2. 增加了一个资源的引用,资源存在依赖性,引用计数是:
    请添加图片描述

  3. 释放资源A,引用计数是:
    请添加图片描述

引用计数为0的,则进行释放操作。


动态引用

静态引用的资源,会被编译器进行序列化后记录在序列化数据中,引擎是可以统计引用关系的, 所以不需要关注内存的释放相关。

但动态引用的资源使用灵活,在需要的时候进行加载。

因为没有序列化,引擎是无法统计引用关系的。导致引用计数为0,就可能出现被误释放的问题。

因此需要借助addRef()decRef()的接口进行手动管理:

const url = 'img_bag/spriteFrame';
resources.load(url, SpriteFrame, (err, spriteFrame) => {
  	if (err) {
      return console.err(err.message);
    }
  	let sprite = this.node.getComponent(Sprite);
  	sprite.spriteFrame = spriteFrame;
    // 增加引用计数,用于保证资源不被错误释放
  	spriteFrame.addRef();
  
  	this._spriteFrame = spriteFrame;
});

// 节点销毁时
protected onDestory() {
  if (this._spriteFrame) {
    this._spriteFrame.decRef();
    this._spriteFrame = null;
  }
}

注意: 配对使用,尤其针对于addRef,如果频繁调用,极大可能出现引用计数非0而内存浪费的问题。


AssetManager

官方提供的AssetManager模块用来负责加载、释放资源相关。在上面的示例中使用引用计数如果忘记,依然存在内存泄漏的问题。

针对于内存管理AssetManager主要提供的接口有:

export class AssetManager {
  // 已加载 bundle 的集合
  bundles: AssetManager.Cache<AssetManager.Bundle>;
  // 获取Bundle
  getBundle(name: string): AssetManager.Bundle | null;
  // 移除Bundle
  removeBundle(bundle: AssetManager.Bundle): void;
  
  // 已加载资源的集合
  assets: AssetManager.Cache<Asset>;
  // 释放资源以及其依赖资源, 不仅会从 assetManager 中删除资源的缓存引用,还会清理它的资源内容
  releaseAsset(asset: Asset): void;
  // 释放所有没有用到的资源
  releaseUnusedAssets(): void;
  // 释放所有资源
  releaseAll(): void;
}

注:只要是Bundle都被AssetManager管理,Bundle和Bundle内的资源移除是两码事

Bundle 在不使用后,如果想移除,需要优先释放 Bundle内的资源。

let bundle = assetManager.getBundle("test_bundle");
if (!bundle) {
  return;
}
// 释放bundle内的所有资源
bundle.releaseAll();
// 移除Bundle
assetManager.removeBundle(bundle);

关于AssetManagerAsset 资源的释放相关,看下引擎的主要实现:

// ../resources/3d/engine/cocos/core/asset-manager/asset-manager.ts
export class AssetManager {
  public releaseAsset (asset: Asset): void {
    releaseManager.tryRelease(asset, true);
  }

  public releaseUnusedAssets () {
    assets.forEach((asset) => {
      releaseManager.tryRelease(asset);
    });
  }

  public releaseAll () {
    assets.forEach((asset) => {
      releaseManager.tryRelease(asset, true);
    });
  }
}

releaseManager.tryRelease的具体实现,看上面release-manager.ts的展示。

除了AssetManager 提供的资源释放以外, Bundle中也存在着一些释放接口,它主要应用于对单一的资源释放。

let bundle = assetManager.getBundle("test_bundle");
if (!bundle) {
  return;
}
// 释放bundle内的单个资源
bundle.release(`image`, SpriteFrame);
// 移除Bundle
assetManager.removeBundle(bundle);

引擎中的主要实现代码:

// ../resources/3d/engine/cocos/core/asset-manager/bundle.ts
export default class Bundle {
  // 释放包内指定路径的资源
  public release (path: string, type?: AssetType | null) {
    const asset = this.get(path, type);
    if (asset) {
      releaseManager.tryRelease(asset, true);
    }
  }
	// 释放包内没有用到的资源
  public releaseUnusedAssets () {
    assets.forEach((asset) => {
      const info = this.getAssetInfo(asset._uuid);
      if (info && !info.redirect) {
        releaseManager.tryRelease(asset);
      }
    });
  }
	// 释放包内所有的资源
  public releaseAll () {
    assets.forEach((asset) => {
      const info = this.getAssetInfo(asset._uuid);
      if (info && !info.redirect) {
        releaseManager.tryRelease(asset, true);
      }
    });
  }
}

releaseManager.tryRelease的具体实现,看上面release-manager.ts的展示。


场景释放

针对于自动释放资源,在场景的 属性检查器 中有个参数叫做 AutoReleaseAssets,勾选。

场景在切换的时候也会进行自动释放该场景下的所有依赖资源。
请添加图片描述

主要的逻辑实现:

  1. director.loadScenedirector.runScene时,它们都会调用runSceneImmediate方法
  2. 该方法会调用关于 release-manager.ts下的接口_autoRelease
// ../resources/3d/engine/cocos/core/asset-manager/release-manager.ts
// 场景的自动释放标记autoReleaseAssets
// 如果为true,表示引用计数-1后进行自动释放,即调用tryRelease接口
public _autoRelease (oldScene: Scene, newScene: Scene, persistNodes: Record<string, Node>) {
  // 检测是否有旧场景
  if (oldScene) {
    const childs = dependUtil.getDeps(oldScene.uuid);
    for (let i = 0, l = childs.length; i < l; i++) {
      const asset = assets.get(childs[i]);
      if (asset) {
        // 重要代码, 如果为true,则调用tryRelease接口
        asset.decRef(TEST || oldScene.autoReleaseAssets);
      }
    }

    const dependencies = dependUtil._depends.get(oldScene.uuid);
    if (dependencies && dependencies.persistDeps) {
      const persistDeps = dependencies.persistDeps;
      for (let i = 0, l = persistDeps.length; i < l; i++) {
        const asset = assets.get(persistDeps[i]);
        if (asset) {
          // 重要代码, 如果为true,则调用tryRelease接口
          asset.decRef(TEST || oldScene.autoReleaseAssets);
        }
      }
    }

    if (oldScene.uuid !== newScene.uuid) {
      dependUtil.remove(oldScene.uuid);
    }
  }
	// ...
}

总结


cocosCreator的资源释放,最后汇总下:

  1. 资源相关的内存管理是引用计数,通过Asset管理
  2. 引用计数相关的逻辑操作,在release-manager.ts
  3. 自动释放的主要代码思想是:将释放的对象保存到临时数组中,且该临时数组的生命周期仅有一帧
  4. 场景相关,建议勾选 AutoReleaseAssets 选项,进行内存自动释放
  5. Bundle相关,建议合理使用releasereleaseUnusedAssetsreleaseAll的接口
  6. AssetManager相关, 释放Bundle的时候,注意资源释放接口的调用(同Bundle名称一样)

最后,祝大家学习生活愉快!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1205575.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

将随机数设成3407,让你的深度学习模型再涨一个点!文再附3种随机数设定方法

随机数重要性 深度学习已经在计算机视觉领域取得了巨大的成功&#xff0c;但我们是否曾想过为什么同样的模型在不同的训练过程中会有不同的表现&#xff1f;为什么使用同样的代码&#xff0c;就是和别人得到的结果不一样&#xff1f;怎么样才能保证自己每次跑同一个实验得到的…

技术管理责任制度《一》

一、技术管理机构责任制 (1) 按各级技术人员的职责范围&#xff0c;分工负责&#xff0c;做好经常性的技术业务工作。 (2) 组织贯彻执行国家有关技术政策和上级办法的技术标准、规定、规程、和各项技术管理制。 (3) 负责收集和提供技术情报、技术资料、技术建议和技术措施等。 …

什么是状态机?

什么是状态机&#xff1f; 定义 我们先来给出状态机的基本定义。一句话&#xff1a; 状态机是有限状态自动机的简称&#xff0c;是现实事物运行规则抽象而成的一个数学模型。 先来解释什么是“状态”&#xff08; State &#xff09;。现实事物是有不同状态的&#xff0c;例…

Spark3.0中的AOE、DPP和Hint增强

1 Spark3.0 AQE Spark 在 3.0 版本推出了 AQE&#xff08;Adaptive Query Execution&#xff09;&#xff0c;即自适应查询执行。AQE 是 Spark SQL 的一种动态优化机制&#xff0c;在运行时&#xff0c;每当 Shuffle Map 阶段执行完毕&#xff0c;AQE 都会结合这个阶段的统计信…

[工业自动化-19]:西门子S7-15xxx编程 - 软件编程 - PLC程序块、组织块OB与PLC多线程原理、OB、FC、FB、DB

目录 一、PLC的块的种类 1.1 什么是块 1.2 块的种类 1.3 不同块之间的相互调用关系 1.4 OB、FC、FB和DB 二、PLC程序组织块OB 2.1 什么是程序块OB 2.2 为什么需要程序块OB 2.3 PLC有哪些程序块 2.4 如何使用程序块 - OB块的执行顺序和规则 2.5 PLC用户程序主函数&am…

2023.11.12使用flask对图片进行黑白处理(base64编码方式传输)

2023.11.12使用flask对图片进行黑白处理&#xff08;base64编码方式传输&#xff09; 由前端输入图片并预览&#xff0c;在后端处理图片后返回前端显示&#xff0c;可以作为图片处理的模板。 关键点在于对图片进行base64编码的转化。 使用Base64编码可以更方便地将图片数据嵌入…

sass 生成辅助色

背景 一个按钮往往有 4 个状态。 默认状态hover鼠标按下禁用状态 为了表示这 4 个状态&#xff0c;需要设置 4 个颜色来提示用户。 按钮类型一般有 5 个&#xff1a; 以 primary 类型按钮为例&#xff0c;设置它不同状态下的颜色&#xff1a; <button class"btn…

【Android】配置Gradle打包apk的环境

目录 生成jks签名文件 配置build.gradle&#xff08;app&#xff09; 打包 生成jks签名文件 Java 密钥库&#xff08;.jks 或 .keystore&#xff09;是用作证书和私钥存储库的二进制文件。用于为用户设备上安装的 APK 签名的密钥。 详细解释请看官方文档&#xff1a; 为应用…

OpenCV踩坑笔记使用笔记入门笔记整合SpringBoot笔记大全

springboot开启摄像头抓拍照片并上传实现&问题记录 NotAllowedErrot: 请求的媒体源不能使用&#xff0c;以下情况会返回该错误: 当前页面内容不安全&#xff0c;没有使用HTTPS没有通过用户授权NotFoundError: 没有找到指定的媒体通道NoReadableError: 访问硬件设备出错Ov…

Linux线程创建,退出,等待

目录​​​​​​​ 一 为什么使用线程 1.1概念 1.2使用线程的理由 二 线程的创建&#xff0c;退出&#xff0c;等待 2.1 线程创建 2.2 线程退出 2.3.线程等待 2.4. 线程ID获取及比较 一 为什么使用线程 1.1概念 概念&#xff1a;"进程——资源分配的最小单位&…

【算法训练-链表 零】链表高频算法题看这一篇就够了

一轮的算法训练完成后&#xff0c;对相关的题目有了一个初步理解了&#xff0c;接下来进行专题训练&#xff0c;以下这些题目就是汇总的高频题目 题目题干直接给出对应博客链接&#xff0c;这里只给出简单思路、代码实现、复杂度分析 反转链表 依据难度等级分别为反转链表、…

2023数字科技生态展,移远通信解锁新成就

11月10日&#xff0c;以“数字科技&#xff0c;焕新启航”为主题的中国电信2023数字科技生态大会暨2023数字科技生态展在广州盛大启幕。作为物联网行业的龙头标杆&#xff0c;同时更与中国电信连续多年维持稳定友好的合作关系&#xff0c;移远通信受邀参加本次展会。 在本次展会…

Docker - DockerFile

Docker - DockerFile DockerFile 描述 dockerfile 是用来构建docker镜像的文件&#xff01;命令参数脚本&#xff01; 构建步骤&#xff1a; 编写一个dockerfile 文件docker build 构建成为一个镜像docker run 运行脚本docker push 发布镜像&#xff08;dockerhub&#xff0…

你真的会使用 MySQL中EXPLAIN吗

EXPLAIN是MySQL数据库中一个强大的工具&#xff0c;用于查询性能分析和优化。通过EXPLAIN&#xff0c;你可以查看MySQL查询的执行计划&#xff0c;了解MySQL是如何执行你的查询语句的。这篇文章将详细介绍EXPLAIN的使用&#xff0c;帮助你更好地理解和优化MySQL查询。 为什么使…

卫星通信和800MHz双管齐下,中国电信对中国移动发起新挑战

依靠国内某科技企业的宣传&#xff0c;卫星通信大热&#xff0c;中国电信也由此成为受益者&#xff0c;日前中国电信又大举招标25万座800MHz 5G基站&#xff0c;显示出中国电信积极以技术优势挑战中国移动。 一、中国电信急起直追 自从4G时代以来&#xff0c;中国电信就在国内通…

web3 React dapp进行事件订阅

好啊&#xff0c;上文web3 React Dapp书写订单 买入/取消操作 我们已经写好了 填充和取消订单 这就已经是非常大的突破了 但是 留下了一个问题 那就是 我们执行完之后 订单的数据没有直接更新 每次都需要我们手动刷新 才能看到结果 那么 今天我们就来看解决这个问题的事件订阅 …

ISP图像处理Pipeline

参考&#xff1a;1. 键盘摄影(七)——深入理解图像信号处理器 ISP2. Understanding ISP Pipeline3. ISP图像处理流程介绍4. ISP系统综述5. ISP(图像信号处理)之——图像处理概述6. ISP 框架7. ISP(图像信号处理)算法概述、工作原理、架构、处理流程8. ISP全流程简介9. ISP流程介…

spring boot中使用Bean Validation做优雅的参数校验

一、Bean Validation简介 Bean Validation是Java定义的一套基于注解的数据校验规范&#xff0c;目前已经从JSR 303的1.0版本升级到JSR 349的1.1版本&#xff0c;再到JSR 380的2.0版本&#xff08;2.0完成于2017.08&#xff09;&#xff0c;目前最新稳定版2.0.2&#xff08;201…

互联网Java工程师面试题·微服务篇·第二弹

目录 18、什么是 Spring 引导的执行器&#xff1f; 19、什么是 Spring Cloud&#xff1f; 20、Spring Cloud 解决了哪些问题&#xff1f; 21、在 Spring MVC 应用程序中使用 WebMvcTest 注释有什么用处&#xff1f; 22、你能否给出关于休息和微服务的要点&#xff1f; 23、…

正点原子嵌入式linux驱动开发——Linux DAC驱动

上一篇笔记中学习了ADC驱动&#xff0c;STM32MP157 也有DAC外设&#xff0c;DAC也使用的IIO驱动框架。本章就来学习一下如下在Linux下使用STM32MP157上的DAC。 DAC简介 ADC是模数转换器&#xff0c;负责将外界的模拟信号转换为数字信号。DAC刚好相反&#xff0c;是数模转换器…