版本: 3.4.0
语言: TypeScript
环境: Mac
回顾
前面有两篇博客说明了:
- cocosCreator 之 resources动态加载、预加载 讲述了静态引用资源,动态加载和预加载相关
- cocosCreator 之 Bundle 讲述了AssetManager关于对内置Bundle和自定义Bundle的使用相关
简单的理解就是对cocosCreator内静态和动态引用资源的使用相关,为了对动态资源更方便管理,增加了AssetManager
用于管理,释放资源相关。
动态引用的资源,相关接口均为异步操作
涉及到资源管理,就会牵扯到资源的内存管理。
在cocosCreator中,官方针对于不同的资源有着不同的内存管理方式。主要有:
- 静态引用资源,通过序列化数据进行自动管理释放
- 动态引用资源,为了避免错误释放而增加引用计数管理, 以及
AssetManager
对资源进行的释放管理 - 场景的自动释放管理
从本质上都是引用计数,但为了有一个更好的理解,故此通过本篇博客汇总出来。
理解可能有误,欢迎您的指出。
引用计数
cocosCreator中的资源都被放在 assets目录下, 主要来源:
- 从外部导入
- 通过远程下载的资源
他们最后都会被包装,使其继承于资源基类: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,则对对象进行释放。
这里有几点需要注意:
- 针对于
this._eventListener
是一个标记,它主要用于保证对象需要在下一帧执行 - 释放操作中的对象增加操作
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中的资源很多都是相互依赖的,他们的引用计数结构类似如下:
-
当使用到某个资源时,引用计数是:
-
增加了一个资源的引用,资源存在依赖性,引用计数是:
-
释放资源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);
关于AssetManager
对 Asset 资源的释放相关,看下引擎的主要实现:
// ../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
,勾选。
场景在切换的时候也会进行自动释放该场景下的所有依赖资源。
主要的逻辑实现:
director.loadScene
或director.runScene
时,它们都会调用runSceneImmediate
方法- 该方法会调用关于 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的资源释放,最后汇总下:
- 资源相关的内存管理是引用计数,通过
Asset
管理 - 引用计数相关的逻辑操作,在
release-manager.ts
中 - 自动释放的主要代码思想是:将释放的对象保存到临时数组中,且该临时数组的生命周期仅有一帧
- 场景相关,建议勾选 AutoReleaseAssets 选项,进行内存自动释放
- Bundle相关,建议合理使用
release
、releaseUnusedAssets
、releaseAll
的接口 - AssetManager相关, 释放Bundle的时候,注意资源释放接口的调用(同Bundle名称一样)
最后,祝大家学习生活愉快!