分享一个 Cocos Creator 2.3.3 内存释放方案

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或者其渠道下载游戏,体验游戏。

2赞



hello,大神,你分享的内存释放方案很棒,仔细查看了,发现有一部分的东西没有权限开放,能帮忙开放拜读一下么?

权限开放了

哈哈哈 没有权限

并没有…

没有开放了

膜拜大佬~