CocosCreator资源管理心得
最近在做项目的资源管理优化,拜读了论坛里面很多优秀的文章:
我写这篇文章主要是探讨工程里面如何做到资源管理自动化
,资源管理简单化
并不是我有什么新的发现。我资源管理的基本原理是基于CocosCreator推荐的方案:
- 自动释放。场景需要勾选自动释放
- 资源引用计数
一、资源管理概述
在CocosCreator体系中,资源分为2个大类
- 静态资源,通俗的讲就是prefab中直接引用的资源。官方的解释如下
当开发者在编辑器中编辑资源时(例如场景、预制件、材质等),
需要在这些资源的属性中配置一些其他的资源,
例如在材质中设置贴图,在场景的 Sprite 组件上设置 SpriteFrame。
那么这些引用关系会被记录在资源的序列化数据中,引擎可以通过这些数据分析出依赖资源列表,
像这样的引用关系就是静态引用。
- 动态资源,就是开发者在运行时动态从AssetBundle中或者远程加载的资源。
这部分资源需要开发者自己管理
二、统一资源管理
在思考项目中应该如何统一资源管理时候,我在想有没有更简单的方式,可以做到:
- 无需项目中每个开发者加载资源时候都要关注到
引用计数
。 - 资源可以被追踪,可以定位到时哪里加载的未释放。
我想到的实现一个统一的资源加载器和追踪器,我们直接看代码
/**
* 资源加载器
*/
export class ResLoader {
public name: string
protected loadResList: Array<Asset>
protected loadResMap: Map<string, Asset> = new Map<string, Asset>()
protected remoteResMap?: Map<string, Asset> = null
constructor(name: string) {
this.name = name
this.loadResList = new Array<Asset>()
}
/**
* 设置图片
*/
public setSpriteFrame(resPath: string, sprite: Sprite, bundleName: string, changeActive: boolean = true, callback?: Function, rootNode?: Node) {
if (rootNode && !isValid(rootNode)) {
return
}
if (!sprite) {
return
}
// 缓存中读取
let key = `${bundleName}::${resPath}`
let asset = this.loadResMap.get(key) as SpriteFrame
if (asset) {
sprite.spriteFrame = asset
if (callback) {
callback()
}
return
}
if (changeActive) {
sprite.node.active = false
}
core.loader.load({
path: resPath,
bundle: bundleName,
type: SpriteFrame,
onComplete: (data) => {
if (!data) {
return
}
data.addRef()
AssetTrace.logAddAssetRef(data, this.name)
if (!isValid(sprite) || (rootNode && !isValid(rootNode))) {
data.decRef()
AssetTrace.logDecAssetRef(data, this.name)
return
}
if (changeActive) {
sprite.node.active = true
}
sprite.spriteFrame = data
this.loadResMap.set(key, data)
this.loadResList.push(data)
if (callback) {
callback()
}
}
})
}
/**
* 加载其他类型资源
*/
public loadAsset<T extends typeof Asset>(resPath: string, bundleName: string, type: T, callback: Function, rootNode?: Node) {
if (rootNode && !isValid(rootNode)) {
return
}
// 缓存中读取
let key = `${bundleName}::${resPath}`
let asset = this.loadResMap.get(key)
if (asset) {
callback(asset)
return
}
core.loader.load({
path: resPath,
bundle: bundleName,
type: type,
onComplete: (asset) => {
if (!asset) {
return
}
asset.addRef()
AssetTrace.logAddAssetRef(asset, this.name)
if ((rootNode && !isValid(rootNode))) {
asset.decRef()
AssetTrace.logDecAssetRef(asset, this.name)
return
}
this.loadResList.push(asset)
this.loadResMap.set(key, asset)
callback(asset)
}
})
}
/**
* 释放资源
*/
public destroy() {
// 延迟0.5s执行
core.scheduler.delayTask(() => {
for (let asset of this.loadResList) {
asset.decRef()
AssetTrace.logDecAssetRef(asset, this.name)
}
for (let asset of this.loadResList) {
if (asset.refCount > 0) {
AssetTrace.addMonitorAsset(asset)
}
}
this.loadResList = []
this.loadResMap.clear()
if (this.remoteResMap != null) {
for (let key of this.remoteResMap.keys()) {
let asset = this.remoteResMap.get(key)
asset.decRef(false)
if (asset.refCount <= 0) {
let spriteFrame = asset as SpriteFrame
if (spriteFrame) {
let texture = spriteFrame.texture as Texture2D;
// 如果已加入动态合图,必须取原始的Texture2D
if (spriteFrame.packable) {
texture = spriteFrame.original?._texture as Texture2D;
}
if (texture) {
texture.image?.decRef();
texture.destroy();
}
spriteFrame.destroy();
} else {
asset.destroy()
}
} else {
AssetTrace.addMonitorAsset(asset)
}
}
this.remoteResMap.clear()
}
}, 0.5)
}
}
以上是核心代码,可以看到我们加载一个资源的过程正是遵从论坛大神推荐的最佳实践。我们的核心思想是:
- 每个界面或者可以统一被管理的资源,定义一个这个加载器
- 所有界面中的资源加载都通过这个加载器加载
- 加载器会记录下每次加载的资源,加载时候
资源计数器+1
- 界面销毁时候调用一下资源加载的
destroy
- 资源加载器destroy时候会负责将自己管理的资源
计数器-1
.并且是延迟卸载,避免一些bug产生 - 资源加载器和监视器同时工作,对于
计数器-1
之后,计数器>0的资源添加到监视器进行监视 - 资源加载计数器+1和资源卸载计数器-1都进行日志记录,方便追踪资源的管理过程
我们再来看看资源监视器
interface AssetMonitorInfo {
tickTimes: number
nextTickTime: number
}
export class AssetTrace {
public static traceSwitch: boolean = false
private static assetSet: Set<Asset> = new Set<Asset>()
private static monitorAsset: Map<Asset, AssetMonitorInfo> = new Map<Asset, AssetMonitorInfo>()
private static initFlag: boolean = false
public static init() {
if (!AssetTrace.traceSwitch) {
return
}
if (AssetTrace.initFlag) {
return
}
AssetTrace.initFlag = true
core.scheduler.intervalTask(AssetTrace.checkAsset, 1)
}
public static enableTrace(enable: boolean) {
AssetTrace.traceSwitch = enable
this.init()
}
public static logAddAssetRef(asset: Asset, loaderName: string) {
if (!AssetTrace.traceSwitch) {
return
}
core.log.info("asset#addRef#{}#{}#{}#{}", loaderName, asset.uuid, asset.name, asset.refCount)
}
public static logDecAssetRef(asset: Asset, loaderName: string) {
if (!AssetTrace.traceSwitch) {
return
}
core.log.info("asset#decRef#{}#{}#{}#{}", loaderName, asset.uuid, asset.name, asset.refCount)
}
public static addMonitorAsset(asset: Asset) {
if (!AssetTrace.traceSwitch) {
return
}
if (AssetTrace.monitorAsset.has(asset)) {
return
}
AssetTrace.monitorAsset.set(asset, {nextTickTime: 0, tickTimes: 0})
}
private static checkAsset() {
if (!AssetTrace.traceSwitch) {
return
}
if (AssetTrace.monitorAsset.size > 0) {
let keys = AssetTrace.monitorAsset.keys()
for (let key of keys) {
let item = AssetTrace.monitorAsset.get(key)
let asset = key
let time = item
if (time.nextTickTime > 0) {
time.nextTickTime -= 1
AssetTrace.monitorAsset.set(asset, time)
continue
}
core.log.info("asset#monitor#{}#{}#{}#{}", "system", asset.uuid, asset.name, asset.refCount)
if (asset.refCount <= 0 || time.tickTimes >= 10) {
AssetTrace.monitorAsset.delete(asset)
continue
} else {
time.tickTimes += 1
time.nextTickTime = time.tickTimes * 2
AssetTrace.monitorAsset.set(asset, time)
}
}
}
}
}
以上是监视器的核心代码
- 监视器有开关控制,可以启动也可以不启动
- 对于需要监视的资源,固定进行1s一次的监视,最多监视10次
这里大家可能有疑问,为什么资源需要进行监视了。这是因为在实践中,对于同一份资源,有可能是被静态引用,也有可能被动态加载。对于动态加载我们是进行计数器管理,可以通过追踪我们自己的计数器监视日志可以追踪,但是如果这个资源又被静态引用,我们是无法监视他的加载的。这个时候我们会发现日志中,该资源引用计数器不是我们预期的变化。会出现类似如下的日志
2025-01-12 10:38:39,359#INFO#asset#addRef#RoleView#94936f61-fcda-4956-8042-05904f141b8e@f9941#sprite_roleNode_bg_2#8
2025-01-12 10:38:39,360#INFO#asset#addRef#RoleView#94936f61-fcda-4956-8042-05904f141b8e@f9941#sprite_roleNode_bg_2#8
2025-01-12 10:38:39,360#INFO#asset#addRef#RoleView#94936f61-fcda-4956-8042-05904f141b8e@f9941#sprite_roleNode_bg_2#8
2025-01-12 10:38:39,360#INFO#asset#addRef#RoleView#94936f61-fcda-4956-8042-05904f141b8e@f9941#sprite_roleNode_bg_2#8
2025-01-12 10:38:39,360#INFO#asset#addRef#RoleView#94936f61-fcda-4956-8042-05904f141b8e@f9941#sprite_roleNode_bg_2#8
2025-01-12 10:38:39,361#INFO#asset#addRef#RoleView#94936f61-fcda-4956-8042-05904f141b8e@f9941#sprite_roleNode_bg_2#8
2025-01-12 10:38:39,361#INFO#asset#addRef#RoleView#94936f61-fcda-4956-8042-05904f141b8e@f9941#sprite_roleNode_bg_2#8
2025-01-12 10:38:40,406#INFO#asset#decRef#RoleView#94936f61-fcda-4956-8042-05904f141b8e@f9941#sprite_roleNode_bg_2#7
2025-01-12 10:38:40,406#INFO#asset#decRef#RoleView#94936f61-fcda-4956-8042-05904f141b8e@f9941#sprite_roleNode_bg_2#6
2025-01-12 10:38:40,406#INFO#asset#decRef#RoleView#94936f61-fcda-4956-8042-05904f141b8e@f9941#sprite_roleNode_bg_2#5
2025-01-12 10:38:40,406#INFO#asset#decRef#RoleView#94936f61-fcda-4956-8042-05904f141b8e@f9941#sprite_roleNode_bg_2#4
2025-01-12 10:38:40,406#INFO#asset#decRef#RoleView#94936f61-fcda-4956-8042-05904f141b8e@f9941#sprite_roleNode_bg_2#3
2025-01-12 10:38:40,406#INFO#asset#decRef#RoleView#94936f61-fcda-4956-8042-05904f141b8e@f9941#sprite_roleNode_bg_2#2
2025-01-12 10:38:40,406#INFO#asset#decRef#RoleView#94936f61-fcda-4956-8042-05904f141b8e@f9941#sprite_roleNode_bg_2#1
2025-01-12 10:39:17,158#INFO#asset#monitor#system#94936f61-fcda-4956-8042-05904f141b8e@f9941##0
- 第一行刚打印时候资源计数器直接8起步,因为静态资源是引擎自动加载,自动进行计数器+1的
- 最后通过计数器-1释放资源,最多只能释放到1,不能到0。
- 静态资源的释放是要等到相关prefab释放之后的下一帧才会释放
- 通过监视器发现最终能够变成0,该资源并未泄露
以上就是我们项目中目前采用的资源管理方式和最终方式。希望能够坛友一起讨论探索!