Cocos Creator 2.3.3 内存释放方案
前言:这里仅提供一种解决内存问题的思路,如有什么表达错误的地方,我会加以修改,感谢。
内存释放方案
参考:
名称定义:
存在静态引用的资源:(prefab,file,anim,plist)等类型的资源。
资源管理类
Loader.ts 资源加载类
其中对 cc.loader.getRes()进行封装,类介绍:
- loadPrefab(path, successCb, failCb) // 预实例化节点;实现已加载出来的预制体的同步调用;
- loadSpriteFrame(path, successCb, failCb) // 动态资源加载
- loadSpine(path, successCb, failCb) // 动态资源加载
- loadSpriteAtlas(path, successCb, failCb) // 动态资源加载
- …
- instantiate(prefabOrNode, realPrefab?) // 实例化接口,保证每个 prefab 实例化时会加组件 AutoRelease.ts 来正确管理引用计数
加载一般的动态资源
loadSpriteFrame(name: string, cb?: (spriteFrame: cc.SpriteFrame) => void, failedCb?: Function) {
// 资源加载保护
Recycle.getInstance().addProtectedCount();
cc.loader.loadRes(name, cc.SpriteFrame, (err, spriteFrame) => {
// 资源加载保护释放
Recycle.getInstance().decProtectedCount();
if (err) {
failedCb && failedCb(err);
return;
}
// 动态资源缓存
DynamicCache.getInstance().addSpriteFrameCache(name, spriteFrame);
cb && cb(spriteFrame);
});
}
加载预制体
loadPrefab(name: string, cb?: Function, failedCb?: Function) {
const prefabName = name.substring(name.lastIndexOf("/") + 1);
const data = CachePrefab.getInstance().getData(prefabName);
if (data) {
if (cc.isValid(data.prefab, true)) {
// 已加载则同步调用
cb && cb(data.prefab);
return;
}
}
Recycle.getInstance().addProtectedCount();
cc.loader.loadRes(name, cc.Prefab, (err, prefab) => {
Recycle.getInstance().decProtectedCount();
if (err) {
failedCb && failedCb(err);
return;
}
// 预实例化节点
CachePrefab.getInstance().updateData(prefab.name, prefab);
cb && cb(prefab);
});
}
由于我们项目规范中不允许存在同名的两个预制体,所以用 prefabName 作为 key,而不是用 path ,这样处理的好处是,我们 resources 下的普通预制体是可以被其他脚本所静态引用的(挂载上去)。
注意:
- 禁止用动态加载接口却不使用资源,特别是要回收的资源(动态图片,动态骨骼)。
- 所有动态加载的位置要做释放保护,防止资源过程中引用计数出错,进行错误的释放,动画餐厅有说明原因。
- 实例化过程中如果只是单纯的复制节点(cc.Node),那么要保证他跟随依附的预制体一起释放掉,因为复制节点是不会增加引用的。
静态资源管理
静态资源引用,其实指的是预制体的引用计数,且要对场景的资源做额外的引用计数。
直接给结论:
- 所有 cc.loader.getRes()替换成 Loader.loadXXX() 的相关的封装接口
- 所有 cc.instantiate 接口替换成 loader.instantiate()接口
- 手动调用 Loader.loadPrefab() 的成功回调里必须保证用 Loader.instantiate()去实例化,保证资源计数正确。
- 游戏所有的场景(*.fire)如果有挂载静态引用的预制体,请手动添加引用计数 Cache.setPersistent();若场景勾选自动回收,要防止静态引用的预制体自动回收 cc.loader.setAutoReleaseRecursively(prefab, false);
- 所有动态加载的接口要加资源加载的保护。
CachePrefab.ts 预制体管理
设计目的:
- 实现预加载的功能,保证调用实例化接口,保证资源计数正确。
- 避免异步接口 Loader.loadPrefab()导致的下一帧才执行回调的问题,比如切换场景黑屏。注意判断接口用 cc.isValid(prefab, true)去判断可用性,第二参数一定要为 true。
interface ICachePrefabData {
prefabName: string; // 预制体名
prefab: cc.Prefab; // 预制体
preloadNode: cc.Node; // 预实例化节点
}
预制体回收
AutoRelease.ts 管理引用计数 在每次实例化之前都要调用一次增加计数的方法
AutoRelease.ts 回收挂载脚本
设计目的:
- 正确管理资源引用。
- 在 Loader.instantiate()方法中手动挂载脚本到预制体节点上,实例化时调用 init()进行计数++;
- 跟随节点销毁而进行引用计数–。
注意:
- 这里加引用的只能是 cc.Prefab 本身,不能是 cc.Node, 因为只能拿到预制体的引用。
- 要保证每个预制体只能挂载一个 AutoRelease.ts 组件
@ccclass
export default class AutoRelease extends cc.Component {
public curPrefab: cc.Prefab = null;
public prefabName: string = null;
init(prefab: cc.Prefab) {
this.curPrefab = prefab;
this.prefabName = prefab.name;
Cache.getInstance().addCache(this.curPrefab);
}
onDestroy() {
Cache.getInstance().decCache(this.curPrefab);
this.curPrefab = null;
}
}
动态资源管理
参考:
前提:
- 动态资源不能被静态引用(resources 下的图片、spine 资源不能被预制体、场景所引用)
- 保证资源加载后,正确添加要加载完资源(loadedCache)和使用中资源(usedCache)。
DynamicCache.ts 动态回收管理脚本
总体思路:在加载的地方把图片、骨骼资源引用起来(只引用一次),然后在赋值的地方把使用的组件引用起来(cc.Sprite,cc.Mask,sp.skeleton),每次回收的时候列表清空所有已销毁或者未赋值的组件,将已加载的资源与正在使用的资源取差集,那部分资源就可以进行释放。 如图:
保证调用方法如下:
// 图片加载
Loader.getInstance().loadSpriteFrame(path, (spriteFrame: cc.SpriteFrame) => {
DynamicCache.getInstance().addSpriteFrameCache(path, spriteFrame);
DynamicCache.getInstance().addSprite(sprite);
}
);
// 骨骼加载
Loader.getInstance().loadSpine(path, (spriteFrame: cc.SpriteFrame) => {
DynamicCache.getInstance().addSpriteFrameCache(path, spriteFrame);
DynamicCache.getInstance().addSprite(spine);
}
);
类说明:
- private addCache(dependence: string[])
- private delCache(dependence: string[])
- public delSpriteCache() // 删除图片引用
- public delSpineCache() // 删除骨骼引用
- public addSpriteFrameCache(path: string, data: cc.SpriteFrame) // 添加已加载的资源
- public addSpineCache(path: string, data: sp.SkeletonData) // 添加已加载的引用
- public addSprite(data: cc.Sprite) // 添加使用中的组件
- public addMask(data: cc.Mask) // 添加使用中的组件
- public addSpine(data: sp.Skeleton) // 添加使用中的组件
- public release(): boolean // 释放
释放策略
RecyclePolicy.ts 回收策略配置
根据机型获得不同的回收配置参数。
回收配置结构:
export interface IRecyclePolicyConfig {
duration: number; // 释放资源间隔(秒)
gcTimes: number; // gc 间隔(次数)
memoryLimit: number; // 内存限制
memoryCheckInterval: number; // 内存回收检测间隔(关闭UI的次数) 防止频繁检测
static: boolean; // 静态资源回收开关
dynamic: boolean; // 动态骨骼回收开关
forceTriggerUI: string[]; // 强制配置的窗口关闭时触发回收
}
// 回收配置
const RecycleConfig = {
[DEVICE_LEVEL.UNKNOWN]: ...,
[DEVICE_LEVEL.LOW]: ...,
[DEVICE_LEVEL.MID]: ...,
[DEVICE_LEVEL.HIGH]: ...,
}
设备等级检测
动态资源检测脚本
目标
检测出(预制体、场景、animation、plist)中所引用的动态资源(resources下的资源)。
实现思路
1.摊开所有.meta文件数据,找到所有的uuid;
2.摊开prefab,file,anim,plist文件数据,与前面的meta通过uuid的匹配;
3.找到可引用文件中在resources下文件。
特别注意:plist文件是xml,需要用 xml2js 做解析,其他的基本都是json
总结
优点
- 满足条件所有资源皆可以得到释放;
- 对于开发者无感知,只需要按规范调用相应的资源加载接口、设置资源接口;
缺点
- 项目需要保证prefab,file,anim,plist等文件不存在对动态资源的引用;
打个广告
该方案已经用在线上游戏 萌鱼泡泡 中,欢迎大家通过taptap或者其渠道下载游戏,体验游戏。