热门标签 | HotTags
当前位置:  开发笔记 > 前端 > 正文

CocosCreator通用框架设计之资源管理

这篇文章主要介绍了CocosCreator通用框架设计之资源管理,对性能优化感兴趣的同学,一定要看一下

如果你想使用Cocos Creator制作一些规模稍大的游戏,那么资源管理是必须解决的问题,随着游戏的进行,你可能会发现游戏的内存占用只升不降,哪怕你当前只用到了极少的资源,并且有使用cc.loader.release来释放之前加载的资源,但之前使用过的大部分资源都会留在内存中!为什么会这样呢?

cocos creator 资源管理存在的问题

资源管理主要解决3个问题,资源加载,资源查找(使用),资源释放。这里要讨论的主要是资源释放的问题,这个问题看上去非常简单,在Cocos2d-x中确实也很简单,但在js中变得复杂了起来,因为难以跟踪一个资源是否可以被释放。

在Cocos2d-x中我们使用引用计数,在引用计数为0的时候释放资源,维护好引用计数即可,而且在Cocos2d-x中我们对资源的管理是比较分散的,引擎层面只提供如TextureCache、AudioManager之类的单例来管理某种特定的资源,大多数的资源都需要我们自己去管理,而在cocos creator中,我们的资源统一由cc.loader来管理,大量使用prefab,prefab与各种资源复杂的引用关系增加了资源管理的难度。

资源依赖

资源A可能依赖资源B、C、D,而资源D又依赖资源E,这是非常常见的一种资源依赖情况,如果我们使用cc.loader.loadRes("A")加载资源A,B~E都会被加载进来,但如果我们调用cc.loader.release("A")则只有资源A被释放。

每一个加载的资源都会放到cc.loader的_cache中,但cc.loader.release只是将传入的资源进行释放,而没有考虑资源依赖的情况。

如果对cc.loader背后的资源加载流程感兴趣可以参考: https://www.cnblogs.com/ybgame/p/10576884.html

如果我们希望将依赖的资源也一起释放,cocos creator提供了一个笨拙的方法,cc.loader.getDependsRecursively;,递归获取指定资源依赖的所有资源,放入一个数组并返回,然后在cc.loader.release中传入该数组,cc.loader会遍历它们,将其逐个释放。

这种方式虽然可以将资源释放,但却有可能释放了不应该释放的资源,如果有一个资源F依赖D,这时候就会导致F资源无法正常工作。由于cocos creator引擎没有维护好资源的依赖,导致我们在释放D的时候并不知道还有F依赖我们。即使没有F依赖,我们也不确定是否可以释放D,比如我们调用cc.loader加载D,而后又加载了A,此时D已经加载完成,A可以直接使用。但如果释放A的时候,将D也释放了,这就不符合我们的预期,我们期望的是在我们没有显式地释放D时,D不应该随着其它资源的释放而自动释放。

可以简单地进行测试,可以打开Chrome的开发者模式,在Console面板中进行输入,如果是旧版本的cocos creator可以在cc.textureCache中dump所有的纹理,而新版本移除了textureCache,但我们可以输入cc.loader._cache来查看所有的资源。如果资源太多,只关心数量,可以输入Object.keys(cc.loader._cache).length来查看资源总数,我们可以在资源加载前dump一次,加载后dump一次,释放后再dump一次,来对比cc.loader中的缓存状态。当然,也可以写一些便捷的方法,如只dump图片,或者dump与上次dump的差异项。

资源使用

除了资源依赖的问题,我们还需要解决资源使用的问题,前者是cc.loader内部的资源组织问题,后者是应用层逻辑的资源使用问题,比如我们需要在一个界面关闭的时候释放某资源,同样会面临一个该不该释放的问题,比如另外一个未关闭的界面是否使用了该资源?如果有其他地方用到了该资源,那么就不应该释放它!

ResLoader

在这里我设计了一个ResLoader,来解决cc.loader没有解决好的问题,关键是为每一个资源创建一个CacheInfo来记录资源的依赖和使用等信息,以此来判断资源是否可以释放,使用ResLoader.getInstance().loadRes()来替代cc.loader.loadRes(),ResLoader.getInstance().releaseRes()来替代cc.loader.releaseRes()。

对于依赖,在资源加载的时候ResLoader会自动建立起映射,释放资源的时候会自动取消映射,并检测取消映射后的资源是否可以释放,是才走释放的逻辑。

对于使用,提供了一个use参数,通过该参数来区别是哪里使用了该资源,以及是否有其他地方使用了该资源,当一个资源即没有倍其他资源依赖,也没有被其它逻辑使用,那么这个资源就可以被释放。

/**
 * 资源加载类
 * 1. 加载完成后自动记录引用关系,根据DependKeys记录反向依赖
 * 2. 支持资源使用,如某打开的UI使用了A资源,其他地方释放资源B,资源B引用了资源A,如果没有其他引用资源A的资源,会触发资源A的释放,
 * 3. 能够安全释放依赖资源(一个资源同时被多个资源引用,只有当其他资源都释放时,该资源才会被释放)
 * 
 * 2018-7-17 by 宝爷
 */

// 资源加载的处理回调
export type ProcessCallback = (completedCount: number, totalCount: number, item: any) => void;
// 资源加载的完成回调
export type CompletedCallback = (error: Error, resource: any) => void;

// 引用和使用的结构体
interface CacheInfo {
    refs: Set,
    uses: Set
}

// LoadRes方法的参数结构
interface LoadResArgs {
    url: string,
    type?: typeof cc.Asset,
    onCompleted?: CompletedCallback,
    onProgess?: ProcessCallback,
    use?: string,
}

// ReleaseRes方法的参数结构
interface ReleaseResArgs {
    url: string,
    type?: typeof cc.Asset,
    use?: string,
}

// 兼容性处理
let isChildClassOf = cc.js["isChildClassOf"]
if (!isChildClassOf) {
    isChildClassOf = cc["isChildClassOf"];
}

export default class ResLoader {

    private _resMap: Map = new Map();
    private static _resLoader: ResLoader = null;
    public static getInstance(): ResLoader {
        if (!this._resLoader) {
            this._resLoader = new ResLoader();
        }
        return this._resLoader;
    }

    public static destroy(): void {
        if (this._resLoader) {
            this._resLoader = null;
        }
    }

    private constructor() {

    }

    /**
     * 从cc.loader中获取一个资源的item
     * @param url 查询的url
     * @param type 查询的资源类型
     */
    private _getResItem(url: string, type: typeof cc.Asset): any {
        let ccloader: any = cc.loader;
        let item = ccloader._cache[url];
        if (!item) {
            let uuid = ccloader._getResUuid(url, type, false);
            if (uuid) {
                let ref = ccloader._getReferenceKey(uuid);
                item = ccloader._cache[ref];
            }
        }
        return item;
    }

    /**
     * loadRes方法的参数预处理
     */
    private _makeLoadResArgs(): LoadResArgs {
        if (arguments.length <1 || typeof arguments[0] != "string") {
            console.error(`_makeLoadResArgs error ${arguments}`);
            return null;
        }
        let ret: LoadResArgs = { url: arguments[0] };
        for (let i = 1; i  i + 1 && typeof arguments[i + 1] == "function") {
                    ret.OnProgess= arguments[i];
                } else {
                    ret.OnCompleted= arguments[i];
                }
            }
        }
        return ret;
    }

    /**
     * releaseRes方法的参数预处理
     */
    private _makeReleaseResArgs(): ReleaseResArgs {
        if (arguments.length <1 || typeof arguments[0] != "string") {
            console.error(`_makeReleaseResArgs error ${arguments}`);
            return null;
        }
        let ret: ReleaseResArgs = { url: arguments[0] };
        for (let i = 1; i (),
                uses: new Set()
            });
        }
        return this._resMap.get(key);
    }

    /**
     * 开始加载资源
     * @param url           资源url
     * @param type          资源类型,默认为null
     * @param onProgess     加载进度回调
     * @param onCompleted   加载完成回调
     * @param use           资源使用key,根据makeUseKey方法生成
     */
    public loadRes(url: string, use&#63;: string);
    public loadRes(url: string, onCompleted: CompletedCallback, use&#63;: string);
    public loadRes(url: string, onProgess: ProcessCallback, onCompleted: CompletedCallback, use&#63;: string);
    public loadRes(url: string, type: typeof cc.Asset, use&#63;: string);
    public loadRes(url: string, type: typeof cc.Asset, onCompleted: CompletedCallback, use&#63;: string);
    public loadRes(url: string, type: typeof cc.Asset, onProgess: ProcessCallback, onCompleted: CompletedCallback, use&#63;: string);
    public loadRes() {
        let resArgs: LoadResArgs = this._makeLoadResArgs.apply(this, arguments);
        console.time("loadRes|"+resArgs.url);
        let finishCallback = (error: Error, resource: any) => {
            // 反向关联引用(为所有引用到的资源打上本资源引用到的标记)
            let addDependKey = (item, refKey) => {
                if (item && item.dependKeys && Array.isArray(item.dependKeys)) {
                    for (let depKey of item.dependKeys) {
                        // 记录该资源被我引用
                        this.getCacheInfo(depKey).refs.add(refKey);
                        // cc.log(`${depKey} ref by ${refKey}`);
                        let ccloader: any = cc.loader;
                        let depItem = ccloader._cache[depKey]
                        addDependKey(depItem, refKey)
                    }
                }
            }

            let item = this._getResItem(resArgs.url, resArgs.type);
            if (item && item.url) {
                addDependKey(item, item.url);
            } else {
                cc.warn(`addDependKey item error1! for ${resArgs.url}`);
            }

            // 给自己加一个自身的引用
            if (item) {
                let info = this.getCacheInfo(item.url);
                info.refs.add(item.url);
                // 更新资源使用
                if (resArgs.use) {
                    info.uses.add(resArgs.use);
                }
            }

            // 执行完成回调
            if (resArgs.onCompleted) {
                resArgs.onCompleted(error, resource);
            }
            console.timeEnd("loadRes|"+resArgs.url);
        };

        // 预判是否资源已加载
        let res = cc.loader.getRes(resArgs.url, resArgs.type);
        if (res) {
            finishCallback(null, res);
        } else {
            cc.loader.loadRes(resArgs.url, resArgs.type, resArgs.onProgess, finishCallback);
        }
    }

    /**
     * 释放资源
     * @param url   要释放的url
     * @param type  资源类型
     * @param use   要解除的资源使用key,根据makeUseKey方法生成
     */
    public releaseRes(url: string, use&#63;: string);
    public releaseRes(url: string, type: typeof cc.Asset, use&#63;: string)
    public releaseRes() {
        /**暂时不释放资源 */
        // return;

        let resArgs: ReleaseResArgs = this._makeReleaseResArgs.apply(this, arguments);
        let item = this._getResItem(resArgs.url, resArgs.type);
        if (!item) {
            console.warn(`releaseRes item is null ${resArgs.url} ${resArgs.type}`);
            return;
        }
        cc.log("resloader release item");
        // cc.log(arguments);
        let cacheInfo = this.getCacheInfo(item.url);
        if (resArgs.use) {
            cacheInfo.uses.delete(resArgs.use)
        }
        this._release(item, item.url);
    }

    // 释放一个资源
    private _release(item, itemUrl) {
        if (!item) {
            return;
        }
        let cacheInfo = this.getCacheInfo(item.url);
        // 解除自身对自己的引用
        cacheInfo.refs.delete(itemUrl);

        if (cacheInfo.uses.size == 0 && cacheInfo.refs.size == 0) {
            // 解除引用
            let delDependKey = (item, refKey) => {
                if (item && item.dependKeys && Array.isArray(item.dependKeys)) {
                    for (let depKey of item.dependKeys) {
                        let ccloader: any = cc.loader;
                        let depItem = ccloader._cache[depKey]
                        this._release(depItem, refKey);
                    }
                }
            }
            delDependKey(item, itemUrl);
            //如果没有uuid,就直接释放url
            if (item.uuid) {
                cc.loader.release(item.uuid);
                cc.log("resloader release item by uuid :" + item.url);
            } else {
                cc.loader.release(item.url);
                cc.log("resloader release item by url:" + item.url);
            }
        }
    }

    /**
     * 判断一个资源能否被释放
     * @param url 资源url
     * @param type  资源类型
     * @param use   要解除的资源使用key,根据makeUseKey方法生成
     */
    public checkReleaseUse(url: string, use&#63;: string): boolean;
    public checkReleaseUse(url: string, type: typeof cc.Asset, use&#63;: string): boolean
    public checkReleaseUse() {
        let resArgs: ReleaseResArgs = this._makeReleaseResArgs.apply(this, arguments);
        let item = this._getResItem(resArgs.url, resArgs.type);
        if (!item) {
            console.log(`cant release,item is null ${resArgs.url} ${resArgs.type}`);
            return true;
        }

        let cacheInfo = this.getCacheInfo(item.url);
        let checkUse = false;
        let checkRef = false;

        if (resArgs.use && cacheInfo.uses.size > 0) {
            if (cacheInfo.uses.size == 1 && cacheInfo.uses.has(resArgs.use)) {
                checkUse = true;
            } else {
                checkUse = false;
            }
        } else {
            checkUse = true;
        }

        if ((cacheInfo.refs.size == 1 && cacheInfo.refs.has(item.url)) || cacheInfo.refs.size == 0) {
            checkRef = true;
        } else {
            checkRef = false;
        }

        return checkUse && checkRef;
    }
}

使用ResLoader

ResLoader的使用非常简单,下面是一个简单的例子,我们可以点击dump按钮来查看当前的资源总数,点击cc.load、cc.release之后分别dump一次,可以发现,开始有36个资源,加载之后有40个资源,而执行释放之后,还有39个资源,只释放了一个资源。

如果使用ResLoader进行测试,发现释放之后只有34个资源,这是因为前面加载场景的资源也被该测试资源依赖,所以这些资源也被释放掉了,只要我们都使用ResLoader来加载和卸载资源,就不会出现资源泄露的问题。

示例代码:

@ccclass
export default class NetExample extends cc.Component {
    @property(cc.Node)
    attachNode: cc.Node = null;
    @property(cc.Label)
    dumpLabel: cc.Label = null;

    onLoadRes() {
        cc.loader.loadRes("Prefab/HelloWorld", cc.Prefab, (error: Error, prefab: cc.Prefab) => {
            if (!error) {
                cc.instantiate(prefab).parent = this.attachNode;
            }
        });
    }

    onUnloadRes() {
        this.attachNode.removeAllChildren(true);
        cc.loader.releaseRes("Prefab/HelloWorld");
    }

    onMyLoadRes() {
        ResLoader.getInstance().loadRes("Prefab/HelloWorld", cc.Prefab, (error: Error, prefab: cc.Prefab) => {
            if (!error) {
                cc.instantiate(prefab).parent = this.attachNode;
            }
        });
    }

    onMyUnloadRes() {
        this.attachNode.removeAllChildren(true);
        ResLoader.getInstance().releaseRes("Prefab/HelloWorld");
    }

    onDump() {
        let Loader:any = cc.loader;
        this.dumpLabel.string = `当前资源总数:${Object.keys(Loader._cache).length}`;
    }
}

可以看到上面的例子是先移除节点,再进行释放,这是正确的使用方式,如果我没有移除直接释放呢??因为释放了纹理,所以cocos creator在接下来的渲染中会不断报错。

ResLoader只是一个基础,直接使用ResLoader我们不需要关心资源的依赖问题,但资源的使用问题我们还需要关心,在实际的使用中,我们可能希望资源的生命周期是以下几种情况:

  • 跟随某对象的生命周期,对象销毁时资源释放
  • 跟随某界面的生命周期,界面关闭时资源释放
  • 跟随某场景的生命周期,场景切换时资源释放

我们可以实现一个组件挂在到对象身上,当我们在该对象或该对象的其它组件中编写逻辑,加载资源时,使用这个资源管理组件进行加载,由该组件来维护资源的释放。界面和场景也类似。。

项目代码位于:https://github.com/wyb10a10/cocos_creator_framework ,打开Scene目录的ResExample场景即可查看。

以上就是CocosCreator通用框架设计之资源管理的详细内容,更多关于CocosCreator框架设计之资源管理的资料请关注其它相关文章!


推荐阅读
  • 技术分享:从动态网站提取站点密钥的解决方案
    本文探讨了如何从动态网站中提取站点密钥,特别是针对验证码(reCAPTCHA)的处理方法。通过结合Selenium和requests库,提供了详细的代码示例和优化建议。 ... [详细]
  • 本文总结了汇编语言中第五至第八章的关键知识点,涵盖间接寻址、指令格式、安全编程空间、逻辑运算指令及数据重复定义等内容。通过详细解析这些内容,帮助读者更好地理解和应用汇编语言的高级特性。 ... [详细]
  • 本文详细介绍了如何使用Python编写爬虫程序,从豆瓣电影Top250页面抓取电影信息。文章涵盖了从基础的网页请求到处理反爬虫机制,再到多页数据抓取的全过程,并提供了完整的代码示例。 ... [详细]
  • 数据库内核开发入门 | 搭建研发环境的初步指南
    本课程将带你从零开始,逐步掌握数据库内核开发的基础知识和实践技能,重点介绍如何搭建OceanBase的开发环境。 ... [详细]
  • DNN Community 和 Professional 版本的主要差异
    本文详细解析了 DotNetNuke (DNN) 的两种主要版本:Community 和 Professional。通过对比两者的功能和附加组件,帮助用户选择最适合其需求的版本。 ... [详细]
  • 尽管某些细分市场如WAN优化表现不佳,但全球运营商路由器和交换机市场持续增长。根据最新研究,该市场预计在2023年达到202亿美元的规模。 ... [详细]
  • 自 Node.js 6.3 版本起,调试功能已内置在核心模块中,无需额外安装 node-inspector 等工具。通过简单的命令即可启动调试模式,并利用 Chrome 浏览器进行高效的代码调试。 ... [详细]
  • 本文探讨了在使用Selenium进行自动化测试时,由于webdriver对象实例化位置不同而导致浏览器闪退的问题,并提供了详细的代码示例和解决方案。 ... [详细]
  • 在跨浏览器开发中,一个常见的问题是关于如何在鼠标悬停时显示图片提示信息。本文深入探讨了 IE 浏览器对 IMG 元素 alt 属性的特殊处理,并提供了最佳实践建议。 ... [详细]
  • Python自动化测试入门:Selenium环境搭建
    本文详细介绍如何在Python环境中安装和配置Selenium,包括开发工具PyCharm的安装、Python环境的设置以及Selenium包的安装方法。此外,还提供了编写和运行第一个自动化测试脚本的步骤。 ... [详细]
  • QBlog开源博客系统:Page_Load生命周期与参数传递优化(第四部分)
    本教程将深入探讨QBlog开源博客系统的Page_Load生命周期,并介绍一种简洁的参数传递重构方法。通过视频演示和详细讲解,帮助开发者更好地理解和应用这些技术。 ... [详细]
  • 在计算机技术的学习道路上,51CTO学院以其专业性和专注度给我留下了深刻印象。从2012年接触计算机到2014年开始系统学习网络技术和安全领域,51CTO学院始终是我信赖的学习平台。 ... [详细]
  • 1:有如下一段程序:packagea.b.c;publicclassTest{privatestaticinti0;publicintgetNext(){return ... [详细]
  • 解决MongoDB Compass远程连接问题
    本文记录了在使用阿里云服务器部署MongoDB后,通过MongoDB Compass进行远程连接时遇到的问题及解决方案。详细介绍了从防火墙配置到安全组设置的各个步骤,帮助读者顺利解决问题。 ... [详细]
  • 你根本不会用百度
    本文转载自第2大脑,详情可以扫描下方二维码关注该公众号摘要:教你正确使用百度。想必你的朋友圈这两天应该被《搜索引擎百度已死》这篇文章刷屏了吧࿰ ... [详细]
author-avatar
mobiledu2502925241
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有