• 售前

  • 售后

热门帖子
入门百科

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

[复制链接]
恶贯满瘾锥 显示全部楼层 发表于 2021-10-26 12:46:54 |阅读模式 打印 上一主题 下一主题
目次


  • cocos creator 资源管理存在的问题
  • 资源依赖
  • 资源使用
  • ResLoader
  • 使用ResLoader
如果你想使用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,这是非常常见的一种资源依赖环境,如果我们使用
  1. cc.loader.loadRes("A")
复制代码
加载资源A,B~E都会被加载进来,但如果我们调用
  1. cc.loader.release("A")
复制代码
则只有资源A被开释。

每一个加载的资源都会放到cc.loader的_cache中,但cc.loader.release只是将传入的资源举行开释,而没有思量资源依赖的环境。
如果对cc.loader背后的资源加载流程感兴趣可以参考: https://www.cnblogs.com/ybgame/p/10576884.html
如果我们盼望将依赖的资源也一起开释,cocos creator提供了一个鸠拙的方法,
  1. 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. /**
  2. * 资源加载类
  3. * 1. 加载完成后自动记录引用关系,根据DependKeys记录反向依赖
  4. * 2. 支持资源使用,如某打开的UI使用了A资源,其他地方释放资源B,资源B引用了资源A,如果没有其他引用资源A的资源,会触发资源A的释放,
  5. * 3. 能够安全释放依赖资源(一个资源同时被多个资源引用,只有当其他资源都释放时,该资源才会被释放)
  6. *
  7. * 2018-7-17 by 宝爷
  8. */
  9. // 资源加载的处理回调
  10. export type ProcessCallback = (completedCount: number, totalCount: number, item: any) => void;
  11. // 资源加载的完成回调
  12. export type CompletedCallback = (error: Error, resource: any) => void;
  13. // 引用和使用的结构体
  14. interface CacheInfo {
  15.     refs: Set<string>,
  16.     uses: Set<string>
  17. }
  18. // LoadRes方法的参数结构
  19. interface LoadResArgs {
  20.     url: string,
  21.     type?: typeof cc.Asset,
  22.     onCompleted?: CompletedCallback,
  23.     onProgess?: ProcessCallback,
  24.     use?: string,
  25. }
  26. // ReleaseRes方法的参数结构
  27. interface ReleaseResArgs {
  28.     url: string,
  29.     type?: typeof cc.Asset,
  30.     use?: string,
  31. }
  32. // 兼容性处理
  33. let isChildClassOf = cc.js["isChildClassOf"]
  34. if (!isChildClassOf) {
  35.     isChildClassOf = cc["isChildClassOf"];
  36. }
  37. export default class ResLoader {
  38.     private _resMap: Map<string, CacheInfo> = new Map<string, CacheInfo>();
  39.     private static _resLoader: ResLoader = null;
  40.     public static getInstance(): ResLoader {
  41.         if (!this._resLoader) {
  42.             this._resLoader = new ResLoader();
  43.         }
  44.         return this._resLoader;
  45.     }
  46.     public static destroy(): void {
  47.         if (this._resLoader) {
  48.             this._resLoader = null;
  49.         }
  50.     }
  51.     private constructor() {
  52.     }
  53.     /**
  54.      * 从cc.loader中获取一个资源的item
  55.      * @param url 查询的url
  56.      * @param type 查询的资源类型
  57.      */
  58.     private _getResItem(url: string, type: typeof cc.Asset): any {
  59.         let ccloader: any = cc.loader;
  60.         let item = ccloader._cache[url];
  61.         if (!item) {
  62.             let uuid = ccloader._getResUuid(url, type, false);
  63.             if (uuid) {
  64.                 let ref = ccloader._getReferenceKey(uuid);
  65.                 item = ccloader._cache[ref];
  66.             }
  67.         }
  68.         return item;
  69.     }
  70.     /**
  71.      * loadRes方法的参数预处理
  72.      */
  73.     private _makeLoadResArgs(): LoadResArgs {
  74.         if (arguments.length < 1 || typeof arguments[0] != "string") {
  75.             console.error(`_makeLoadResArgs error ${arguments}`);
  76.             return null;
  77.         }
  78.         let ret: LoadResArgs = { url: arguments[0] };
  79.         for (let i = 1; i < arguments.length; ++i) {
  80.             if (i == 1 && isChildClassOf(arguments[i], cc.RawAsset)) {
  81.                 // 判断是不是第一个参数type
  82.                 ret.type = arguments[i];
  83.             } else if (i == arguments.length - 1 && typeof arguments[i] == "string") {
  84.                 // 判断是不是最后一个参数use
  85.                 ret.use = arguments[i];
  86.             } else if (typeof arguments[i] == "function") {
  87.                 // 其他情况为函数
  88.                 if (arguments.length > i + 1 && typeof arguments[i + 1] == "function") {
  89.                     ret.onProgess = arguments[i];
  90.                 } else {
  91.                     ret.onCompleted = arguments[i];
  92.                 }
  93.             }
  94.         }
  95.         return ret;
  96.     }
  97.     /**
  98.      * releaseRes方法的参数预处理
  99.      */
  100.     private _makeReleaseResArgs(): ReleaseResArgs {
  101.         if (arguments.length < 1 || typeof arguments[0] != "string") {
  102.             console.error(`_makeReleaseResArgs error ${arguments}`);
  103.             return null;
  104.         }
  105.         let ret: ReleaseResArgs = { url: arguments[0] };
  106.         for (let i = 1; i < arguments.length; ++i) {
  107.             if (typeof arguments[i] == "string") {
  108.                 ret.use = arguments[i];
  109.             } else {
  110.                 ret.type = arguments[i];
  111.             }
  112.         }
  113.         return ret;
  114.     }
  115.     /**
  116.      * 生成一个资源使用Key
  117.      * @param where 在哪里使用,如Scene、UI、Pool
  118.      * @param who 使用者,如Login、UIHelp...
  119.      * @param why 使用原因,自定义...
  120.      */
  121.     public static makeUseKey(where: string, who: string = "none", why: string = ""): string {
  122.         return `use_${where}_by_${who}_for_${why}`;
  123.     }
  124.     /**
  125.      * 获取资源缓存信息
  126.      * @param key 要获取的资源url
  127.      */
  128.     public getCacheInfo(key: string): CacheInfo {
  129.         if (!this._resMap.has(key)) {
  130.             this._resMap.set(key, {
  131.                 refs: new Set<string>(),
  132.                 uses: new Set<string>()
  133.             });
  134.         }
  135.         return this._resMap.get(key);
  136.     }
  137.     /**
  138.      * 开始加载资源
  139.      * @param url           资源url
  140.      * @param type          资源类型,默认为null
  141.      * @param onProgess     加载进度回调
  142.      * @param onCompleted   加载完成回调
  143.      * @param use           资源使用key,根据makeUseKey方法生成
  144.      */
  145.     public loadRes(url: string, use?: string);
  146.     public loadRes(url: string, onCompleted: CompletedCallback, use?: string);
  147.     public loadRes(url: string, onProgess: ProcessCallback, onCompleted: CompletedCallback, use?: string);
  148.     public loadRes(url: string, type: typeof cc.Asset, use?: string);
  149.     public loadRes(url: string, type: typeof cc.Asset, onCompleted: CompletedCallback, use?: string);
  150.     public loadRes(url: string, type: typeof cc.Asset, onProgess: ProcessCallback, onCompleted: CompletedCallback, use?: string);
  151.     public loadRes() {
  152.         let resArgs: LoadResArgs = this._makeLoadResArgs.apply(this, arguments);
  153.         console.time("loadRes|"+resArgs.url);
  154.         let finishCallback = (error: Error, resource: any) => {
  155.             // 反向关联引用(为所有引用到的资源打上本资源引用到的标记)
  156.             let addDependKey = (item, refKey) => {
  157.                 if (item && item.dependKeys && Array.isArray(item.dependKeys)) {
  158.                     for (let depKey of item.dependKeys) {
  159.                         // 记录该资源被我引用
  160.                         this.getCacheInfo(depKey).refs.add(refKey);
  161.                         // cc.log(`${depKey} ref by ${refKey}`);
  162.                         let ccloader: any = cc.loader;
  163.                         let depItem = ccloader._cache[depKey]
  164.                         addDependKey(depItem, refKey)
  165.                     }
  166.                 }
  167.             }
  168.             let item = this._getResItem(resArgs.url, resArgs.type);
  169.             if (item && item.url) {
  170.                 addDependKey(item, item.url);
  171.             } else {
  172.                 cc.warn(`addDependKey item error1! for ${resArgs.url}`);
  173.             }
  174.             // 给自己加一个自身的引用
  175.             if (item) {
  176.                 let info = this.getCacheInfo(item.url);
  177.                 info.refs.add(item.url);
  178.                 // 更新资源使用
  179.                 if (resArgs.use) {
  180.                     info.uses.add(resArgs.use);
  181.                 }
  182.             }
  183.             // 执行完成回调
  184.             if (resArgs.onCompleted) {
  185.                 resArgs.onCompleted(error, resource);
  186.             }
  187.             console.timeEnd("loadRes|"+resArgs.url);
  188.         };
  189.         // 预判是否资源已加载
  190.         let res = cc.loader.getRes(resArgs.url, resArgs.type);
  191.         if (res) {
  192.             finishCallback(null, res);
  193.         } else {
  194.             cc.loader.loadRes(resArgs.url, resArgs.type, resArgs.onProgess, finishCallback);
  195.         }
  196.     }
  197.     /**
  198.      * 释放资源
  199.      * @param url   要释放的url
  200.      * @param type  资源类型
  201.      * @param use   要解除的资源使用key,根据makeUseKey方法生成
  202.      */
  203.     public releaseRes(url: string, use?: string);
  204.     public releaseRes(url: string, type: typeof cc.Asset, use?: string)
  205.     public releaseRes() {
  206.         /**暂时不释放资源 */
  207.         // return;
  208.         let resArgs: ReleaseResArgs = this._makeReleaseResArgs.apply(this, arguments);
  209.         let item = this._getResItem(resArgs.url, resArgs.type);
  210.         if (!item) {
  211.             console.warn(`releaseRes item is null ${resArgs.url} ${resArgs.type}`);
  212.             return;
  213.         }
  214.         cc.log("resloader release item");
  215.         // cc.log(arguments);
  216.         let cacheInfo = this.getCacheInfo(item.url);
  217.         if (resArgs.use) {
  218.             cacheInfo.uses.delete(resArgs.use)
  219.         }
  220.         this._release(item, item.url);
  221.     }
  222.     // 释放一个资源
  223.     private _release(item, itemUrl) {
  224.         if (!item) {
  225.             return;
  226.         }
  227.         let cacheInfo = this.getCacheInfo(item.url);
  228.         // 解除自身对自己的引用
  229.         cacheInfo.refs.delete(itemUrl);
  230.         if (cacheInfo.uses.size == 0 && cacheInfo.refs.size == 0) {
  231.             // 解除引用
  232.             let delDependKey = (item, refKey) => {
  233.                 if (item && item.dependKeys && Array.isArray(item.dependKeys)) {
  234.                     for (let depKey of item.dependKeys) {
  235.                         let ccloader: any = cc.loader;
  236.                         let depItem = ccloader._cache[depKey]
  237.                         this._release(depItem, refKey);
  238.                     }
  239.                 }
  240.             }
  241.             delDependKey(item, itemUrl);
  242.             //如果没有uuid,就直接释放url
  243.             if (item.uuid) {
  244.                 cc.loader.release(item.uuid);
  245.                 cc.log("resloader release item by uuid :" + item.url);
  246.             } else {
  247.                 cc.loader.release(item.url);
  248.                 cc.log("resloader release item by url:" + item.url);
  249.             }
  250.         }
  251.     }
  252.     /**
  253.      * 判断一个资源能否被释放
  254.      * @param url 资源url
  255.      * @param type  资源类型
  256.      * @param use   要解除的资源使用key,根据makeUseKey方法生成
  257.      */
  258.     public checkReleaseUse(url: string, use?: string): boolean;
  259.     public checkReleaseUse(url: string, type: typeof cc.Asset, use?: string): boolean
  260.     public checkReleaseUse() {
  261.         let resArgs: ReleaseResArgs = this._makeReleaseResArgs.apply(this, arguments);
  262.         let item = this._getResItem(resArgs.url, resArgs.type);
  263.         if (!item) {
  264.             console.log(`cant release,item is null ${resArgs.url} ${resArgs.type}`);
  265.             return true;
  266.         }
  267.         let cacheInfo = this.getCacheInfo(item.url);
  268.         let checkUse = false;
  269.         let checkRef = false;
  270.         if (resArgs.use && cacheInfo.uses.size > 0) {
  271.             if (cacheInfo.uses.size == 1 && cacheInfo.uses.has(resArgs.use)) {
  272.                 checkUse = true;
  273.             } else {
  274.                 checkUse = false;
  275.             }
  276.         } else {
  277.             checkUse = true;
  278.         }
  279.         if ((cacheInfo.refs.size == 1 && cacheInfo.refs.has(item.url)) || cacheInfo.refs.size == 0) {
  280.             checkRef = true;
  281.         } else {
  282.             checkRef = false;
  283.         }
  284.         return checkUse && checkRef;
  285.     }
  286. }
复制代码
使用ResLoader

ResLoader的使用非常简朴,下面是一个简朴的例子,我们可以点击dump按钮来查察当前的资源总数,点击cc.load、cc.release之后分别dump一次,可以发现,开始有36个资源,加载之后有40个资源,而执行开释之后,还有39个资源,只开释了一个资源。
如果使用ResLoader举行测试,发现开释之后只有34个资源,这是因为前面加载场景的资源也被该测试资源依赖,所以这些资源也被开释掉了,只要我们都使用ResLoader来加载和卸载资源,就不会出现资源泄露的问题。

示例代码:
  1. @ccclass
  2. export default class NetExample extends cc.Component {
  3.     @property(cc.Node)
  4.     attachNode: cc.Node = null;
  5.     @property(cc.Label)
  6.     dumpLabel: cc.Label = null;
  7.     onLoadRes() {
  8.         cc.loader.loadRes("Prefab/HelloWorld", cc.Prefab, (error: Error, prefab: cc.Prefab) => {
  9.             if (!error) {
  10.                 cc.instantiate(prefab).parent = this.attachNode;
  11.             }
  12.         });
  13.     }
  14.     onUnloadRes() {
  15.         this.attachNode.removeAllChildren(true);
  16.         cc.loader.releaseRes("Prefab/HelloWorld");
  17.     }
  18.     onMyLoadRes() {
  19.         ResLoader.getInstance().loadRes("Prefab/HelloWorld", cc.Prefab, (error: Error, prefab: cc.Prefab) => {
  20.             if (!error) {
  21.                 cc.instantiate(prefab).parent = this.attachNode;
  22.             }
  23.         });
  24.     }
  25.     onMyUnloadRes() {
  26.         this.attachNode.removeAllChildren(true);
  27.         ResLoader.getInstance().releaseRes("Prefab/HelloWorld");
  28.     }
  29.     onDump() {
  30.         let Loader:any = cc.loader;
  31.         this.dumpLabel.string = `当前资源总数:${Object.keys(Loader._cache).length}`;
  32.     }
  33. }
复制代码
可以看到上面的例子是先移除节点,再举行开释,这是精确的使用方式,如果我没有移除直接开释呢??因为开释了纹理,所以cocos creator在接下来的渲染中会不断报错。
ResLoader只是一个底子,直接使用ResLoader我们不需要关心资源的依赖问题,但资源的使用问题我们还需要关心,在实际的使用中,我们大概盼望资源的生命周期是以下几种环境:
       
  • 跟随某对象的生命周期,对象销毁时资源开释   
  • 跟随某界面的生命周期,界面关闭时资源开释   
  • 跟随某场景的生命周期,场景切换时资源开释
我们可以实现一个组件挂在到对象身上,当我们在该对象或该对象的别的组件中编写逻辑,加载资源时,使用这个资源管理组件举行加载,由该组件来维护资源的开释。界面和场景也类似。。
项目代码位于:https://github.com/wyb10a10/cocos_creator_framework ,打开Scene目次的ResExample场景即可查察。
以上就是CocosCreator通用框架计划之资源管理的具体内容,更多关于CocosCreator框架计划之资源管理的资料请关注脚本之家别的干系文章!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

帖子地址: 

回复

使用道具 举报

分享
推广
火星云矿 | 预约S19Pro,享500抵1000!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

草根技术分享(草根吧)是全球知名中文IT技术交流平台,创建于2021年,包含原创博客、精品问答、职业培训、技术社区、资源下载等产品服务,提供原创、优质、完整内容的专业IT技术开发社区。
  • 官方手机版

  • 微信公众号

  • 商务合作