当前位置: 首页 > news >正文

cocosCreator 之内存管理和释放

版本: 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;}// 引用计数+1public 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) {// 检测引用计数和是否存在循环引用,如果存在则returnif (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++) {// 对象有效,则进行引用计数-1const dependAsset = assets.get(depends[i]);if (dependAsset) {dependAsset.decRef(false);// no need to release dependencies recursively in editorif (!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>;// 获取BundlegetBundle(name: string): AssetManager.Bundle | null;// 移除BundleremoveBundle(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名称一样)

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

相关文章:

cocosCreator 之内存管理和释放

版本&#xff1a; 3.4.0 语言&#xff1a; TypeScript 环境&#xff1a; Mac 回顾 前面有两篇博客说明了&#xff1a; cocosCreator 之 resources动态加载、预加载 讲述了静态引用资源&#xff0c;动态加载和预加载相关cocosCreator 之 Bundle 讲述了AssetManager关于对内置…...

飞天使-template模版相关知识

遇到报错django.template.exceptions.TemplateSyntaxError: ‘staticfiles’ is not a registered tag library. Must ROOT_URLCONF TEMPLATES [{BACKEND: django.template.backends.django.DjangoTemplates,DIRS: [os.path.join(BASE_DIR, templates)],APP_DIRS: True,OPTI…...

一、Hadoop3.1.3集群搭建

一、集群规划 hadoop01(209.2)hadoop02(209.3)hadoop03(209.4)HDFSNameNode DataNodeDataNodeSecondaryNameNode DataNodeYARNNodeManagerResourceManager NodeManagerNodeManager NameNode和SecondaryNameNode不要放在同一台服务器上 二、创建用户 useradd atguigu passwd *…...

QML16、从 C++ 定义 QML 类型

从 C++ 定义 QML 类型 当使用 C++ 代码扩展 QML 时,可以向 QML 类型系统注册 C++ 类,以使该类能够用作 QML 代码中的数据类型。 虽然任何 QObject 派生类的属性、方法和信号都可以从 QML 访问,如将 C++ 类型的属性暴露给 QML 中所讨论的,但在向类型系统注册之前,此类类不能…...

【中间件篇-Redis缓存数据库06】Redis主从复制/哨兵 高并发高可用

Redis高并发高可用 复制 在分布式系统中为了解决单点问题&#xff0c;通常会把数据复制多个副本部署到其他机器&#xff0c;满足故障恢复和负载均衡等需求。Redis也是如此&#xff0c;它为我们提供了复制功能&#xff0c;实现了相同数据的多个Redis 副本。复制功能是高可用Re…...

LeetCode(12)时间插入、删除和获取随机元素【数组/字符串】【中等】

目录 1.题目2.答案3.提交结果截图 链接&#xff1a; 380. O(1) 时间插入、删除和获取随机元素 1.题目 实现RandomizedSet 类&#xff1a; RandomizedSet() 初始化 RandomizedSet 对象bool insert(int val) 当元素 val 不存在时&#xff0c;向集合中插入该项&#xff0c;并返回…...

前端面试题 计算机网络

文章目录 ios 7层协议tcp协议和udp协议的区别tcp协议如何确保数据的可靠http和tcp的关系url输入地址到呈现网页有哪些步骤post和get本质区别&#xff0c;什么时候会触发二次预检GET请求&#xff1a;POST请求&#xff1a;触发二次预检&#xff08;CORS中的预检请求&#xff09;&…...

windows aseprite编译指南(白嫖)

aseprite是画像素图的专业软件&#xff0c;steam上有售卖&#xff0c;不过官方也在github开源了&#xff0c;需要自己编译。 1. 首先获取源码 直接在github上clone源码到本地指定目录 需要先下载git&#xff0c;下载好后在git.bash中执行&#xff08;需要腾一个用来安放源码的…...

生活污水处理一体化处理设备有哪些

生活污水处理一体化处理设备有多种类型&#xff0c;包括但不限于以下几种&#xff1a; 鼓风机&#xff1a;提供曝气系统所需的气流。潜水污水提升泵&#xff1a;将污水从低处提升到高处。旋转式滚筒筛分机&#xff1a;对污水中的悬浮物进行分离和筛选。回旋式格栅&#xff1a;…...

JSON可视化管理工具JSON Hero

本文软件由网友 zxc 推荐&#xff1b; 什么是 JSON Hero &#xff1f; JSON Hero 是一个简单实用的 JSON 工具&#xff0c;通过简介美观的 UI 及增强的额外功能&#xff0c;使得阅读和理解 JSON 文档变得更容易、直观。 主要功能 支持多种视图以便查看 JSON&#xff1a;列视图…...

P6入门:项目初始化7-项目详情之代码/分类码Code

前言 使用项目详细信息查看和编辑有关所选项目的详细信息&#xff0c;在项目创建完成后&#xff0c;初始化项目是一项非常重要的工作&#xff0c;涉及需要设置的内容包括项目名&#xff0c;ID,责任人&#xff0c;日历&#xff0c;预算&#xff0c;资金&#xff0c;分类码等等&…...

跨国企业如何选择安全靠谱的跨国传输文件软件?

随着全球化的不断发展&#xff0c;跨国企业之间的合作变得越来越频繁。而在这种合作中&#xff0c;如何安全、可靠地将文件传输给合作伙伴或客户&#xff0c;成为了跨国企业必须面对的问题。 然而&#xff0c;跨国文件传输并不是一件容易的事情&#xff0c;由于网络物理条件的…...

Command Injection

Command Injection "Command Injection"(命令注入)&#xff0c;其目标是通过一个应用程序在主机操作系统上执行任意命令。当一个应用程序将用户提供的数据&#xff08;如表单、cookies、HTTP头等&#xff09;传递给系统shell时&#xff0c;就可能发生命令注入攻击。在…...

LeetCode | 20. 有效的括号

LeetCode | 20. 有效的括号 OJ链接 这道题可以使用栈来解决问题~~ 思路&#xff1a; 首先我们要使用我们之前写的栈的实现来解决此问题~~如果左括号&#xff0c;就入栈如果右括号&#xff0c;出栈顶的左括号跟右括号判断是否匹配 如果匹配&#xff0c;继续如果不匹配&#…...

英语语法 - 祈使句 | 虚拟语气

目录 [ 祈使句 ] 1. [ 及物动词原形 宾语 (状语) | 不及物动词原形 (状语) | be 表语 (状语) ] 2. [ Dont 及物动词原形 宾语 | dont 不及物动词原形 ] 3. [ dont be 表语 ] 4. 特殊 you [ 虚拟语气 ] 1. [ 条件状语从句 - 虚拟语气 ] 现在时态虚拟语气 将来…...

记录pytorch实现自定义算子并转onnx文件输出

概览&#xff1a;记录了如何自定义一个算子&#xff0c;实现pytorch注册&#xff0c;通过C编译为库文件供python端调用&#xff0c;并转为onnx文件输出 整体大概流程&#xff1a; 定义算子实现为torch的C版本文件注册算子编译算子生成库文件调用自定义算子 一、编译环境准备…...

ARPG----C++学习记录04 Section8 角色类,移动

角色类输入 新建一个角色C&#xff0c;继承建立蓝图,和Pawn一样&#xff0c;绑定输入移动和相机. 在构造函数中添加这段代码也能实现。打开UsePawnControlRotation就可以让人物不跟随鼠标旋转 得到旋转后的向前向量 使用旋转矩阵 想要前进方向和旋转的方向对应。获取当前控制…...

拆解软件定义汽车:OS突围

软件作为智能汽车的核心组成部分&#xff0c;由于自身较为独立和复杂的IT学科体系&#xff0c;其技术链路、产业分工、价值分配、商业模式相对硬件产品&#xff08;如域控、激光雷达、摄像头等硬件&#xff09;而言&#xff0c;在汽车产业内探讨和传播相对较少。 11月3日&…...

并发线程使用介绍(二)

2.2.6 线程的强占 Thread的非静态方法join方法 需要在某一个线程下去调用这个方法 如果在main线程中调用了t1.join()&#xff0c;那么main线程会进入到等待状态&#xff0c;需要等待t1线程全部执行完毕&#xff0c;在恢复到就绪状态等待 CPU调度。 如果在main线程中调用了t1.j…...

【Proteus仿真】【51单片机】多路温度控制系统

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真51单片机控制器&#xff0c;使用按键、LED、蜂鸣器、LCD1602、DS18B20温度传感器、HC05蓝牙模块等。 主要功能&#xff1a; 系统运行后&#xff0c;默认LCD1602显示前4路采集的温…...

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…...

通过Wrangler CLI在worker中创建数据库和表

官方使用文档&#xff1a;Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后&#xff0c;会在本地和远程创建数据库&#xff1a; npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库&#xff1a; 现在&#xff0c;您的Cloudfla…...

江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命

在华东塑料包装行业面临限塑令深度调整的背景下&#xff0c;江苏艾立泰以一场跨国资源接力的创新实践&#xff0c;重新定义了绿色供应链的边界。 跨国回收网络&#xff1a;废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点&#xff0c;将海外废弃包装箱通过标准…...

Web 架构之 CDN 加速原理与落地实践

文章目录 一、思维导图二、正文内容&#xff08;一&#xff09;CDN 基础概念1. 定义2. 组成部分 &#xff08;二&#xff09;CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 &#xff08;三&#xff09;CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 &#xf…...

九天毕昇深度学习平台 | 如何安装库?

pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子&#xff1a; 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...

【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的“no matching...“系列算法协商失败问题

【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的"no matching..."系列算法协商失败问题 摘要&#xff1a; 近期&#xff0c;在使用较新版本的OpenSSH客户端连接老旧SSH服务器时&#xff0c;会遇到 "no matching key exchange method found"​, "n…...

Kafka入门-生产者

生产者 生产者发送流程&#xff1a; 延迟时间为0ms时&#xff0c;也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于&#xff1a;异步发送不需要等待结果&#xff0c;同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...

9-Oracle 23 ai Vector Search 特性 知识准备

很多小伙伴是不是参加了 免费认证课程&#xff08;限时至2025/5/15&#xff09; Oracle AI Vector Search 1Z0-184-25考试&#xff0c;都顺利拿到certified了没。 各行各业的AI 大模型的到来&#xff0c;传统的数据库中的SQL还能不能打&#xff0c;结构化和非结构的话数据如何和…...

如何把工业通信协议转换成http websocket

1.现状 工业通信协议多数工作在边缘设备上&#xff0c;比如&#xff1a;PLC、IOT盒子等。上层业务系统需要根据不同的工业协议做对应开发&#xff0c;当设备上用的是modbus从站时&#xff0c;采集设备数据需要开发modbus主站&#xff1b;当设备上用的是西门子PN协议时&#xf…...

大模型——基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程

基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程 下载安装Docker Docker官网:https://www.docker.com/ 自定义Docker安装路径 Docker默认安装在C盘,大小大概2.9G,做这行最忌讳的就是安装软件全装C盘,所以我调整了下安装路径。 新建安装目录:E:\MyS…...