• 售前

  • 售后

热门帖子
入门百科

分析CocosCreator新资源管理体系

[复制链接]
风吹吹蛋蛋疼风w 显示全部楼层 发表于 2021-10-26 12:23:28 |阅读模式 打印 上一主题 下一主题
目录


  • 1.资源与构建

    • 1.1 creator资源文件根本
    • 1.2 资源构建

  • 2. 理解与利用 Asset Bundle

    • 2.1 创建Bundle
    • 2.2 利用Bundle

  • 3. 新资源框架剖析

    • 3.1 加载管线
    • 3.2 文件下载
    • 3.3 文件解析
    • 3.4 依靠加载
    • 3.5 资源释放


1.资源与构建


1.1 creator资源文件根本

在相识引擎怎样解析、加载资源之前,我们先来相识一下这些资源文件(图片、Prefab、动画等)的规则,在creator项目目录下有几个与资源相干的目录:
       
  • assets 所有资源的总目录,对应creator编辑器的资源管理器   
  • library 本地资源库,预览项目时利用的目录   
  • build 构建后的项目默认目录

在assets目录下,creator会为每个资源文件和目录天生一个同名的.meta文件,meta文件是一个json文件,记录了资源的版本、uuid以及各种自定义的信息(在编辑器的
  1. 属性检查器
复制代码
中设置),比如prefab的meta文件,就记录了我们可以在编辑器修改的optimizationPolicy和asyncLoadAssets等属性。
  1. {
  2.   "ver": "1.2.7",
  3.   "uuid": "a8accd2e-6622-4c31-8a1e-4db5f2b568b5",
  4.   "optimizationPolicy": "AUTO",     // prefab创建优化策略
  5.   "asyncLoadAssets": false,         // 是否延迟加载
  6.   "readonly": false,
  7.   "subMetas": {}
  8. }
复制代码
在library目录下的imports目录,资源文件名会被转换成uuid,并取uuid前2个字符进行目录分组存放,creator会将所有资源的uuid到assets目录的映射关系,以及资源和meta的末了更新时间戳放到一个名为uuid-to-mtime.json的文件中,如下所示。
  1. {
  2.   "9836134e-b892-4283-b6b2-78b5acf3ed45": {
  3.     "asset": 1594351233259,
  4.     "meta": 1594351616611,
  5.     "relativePath": "effects"
  6.   },
  7.   "430eccbf-bf2c-4e6e-8c0c-884bbb487f32": {
  8.     "asset": 1594351233254,
  9.     "meta": 1594351616643,
  10.     "relativePath": "effects\\__builtin-editor-gizmo-line.effect"
  11.   },
  12.   ...
  13. }
复制代码
与assets目录下的资源相比,library目录下的资源归并了meta文件的信息。文件目录则只在uuid-to-mtime.json中记录,library目录并没有为目录天生任何东西。

1.2 资源构建

在项目构建之后,资源会从library目录下移动到构建输出的build目录中,根本只会导出参与构建的场景和resources目录下的资源,及其引用到的资源。脚本资源会由多个js脚本归并为一个js,各种json文件也会按照特定的规则进行打包。我们可以在Bundle的设置界面和项目标构建界面为Bundle和项目设置

1.2.1 图片、图集、主动图集
       
  • https://docs.cocos.com/creator/manual/zh/asset-workflow/sprite.html   
  • https://docs.cocos.com/creator/manual/zh/asset-workflow/atlas.html   
  • https://docs.cocos.com/creator/manual/zh/asset-workflow/auto-atlas.html
导入编辑器的每张图片都会对应天生一个json文件,用于描述Texture的信息,如下所示,默认环境下项目中所有的Texture2D的json文件会被压缩成一个,假如选择
  1. 无压缩
复制代码
,则每个图片都会天生一个Texture2D的json文件。
  1. {
  2.   "__type__": "cc.Texture2D",
  3.   "content": "0,9729,9729,33071,33071,0,0,1"
  4. }
复制代码
假如将纹理的Type属性设置为Sprite,Creator还会主动天生了SpriteFrame范例的json文件。
图集资源除了图片外,还对应一个图集json,这个json包罗了cc.SpriteAtlas信息,以及每个碎图的SpriteFrame信息
主动图集在默认环境下只包罗了cc.SpriteAtlas信息,在勾选内联所有SpriteFrame的环境下,会归并所有SpriteFrame
1.2.2 Prefab与场景
       
  • https://docs.cocos.com/creator/manual/zh/asset-workflow/prefab.html   
  • https://docs.cocos.com/creator/manual/zh/asset-workflow/scene-managing.html
场景资源与Prefab资源非常类似,都是一个描述了所有节点、组件等信息的json文件,在勾选
  1. 内联所有SpriteFrame
复制代码
的环境下,Prefab引用到的SpriteFrame会被归并到prefab所在的json文件中,假如一个SpriteFrame被多个prefab引用,那么每个prefab的json文件都会包罗该SpriteFrame的信息。而在没有勾选
  1. 内联所有SpriteFrame
复制代码
的环境下,SpriteFrame会是单独的json文件。
1.2.3 资源文件归并规则
当Creator将多个资源归并到一个json文件中,我们可以在config.json中的packs字段找到被
  1. 打包
复制代码
的资源信息,一个资源有大概被重复打包到多个json中。下面举一个例子,展示在差别的选项下,creator的构建规则:
       
  • a.png 一个单独的Sprite范例图片   
  • dir/b.png、c.png、AutoAtlas dir目录下包罗2张图片,以及一个AutoAtlas   
  • d.png、d.plist 平凡图集   
  • e.prefab 引用了SpriteFrame a和b的prefab   
  • f.prefab 引用了SpriteFrame b的prefab
下面是按差别规则构建后的文件,可以看到,无压缩的环境下天生的文件数量是最多的,不内联的文件会比内联多,但内联大概会导致同一个文件被重复包罗,比如e和f这两个Prefab都引用了同一个图片,这个图片的SpriteFrame.json会被重复包罗,归并成一个json则只会天生一个文件。
            资源文件            无压缩            默认(不内联)            默认(内联)            归并json                                    a.png            a.texture.json + a.spriteframe.json            a.spriteframe.json                                                    ./dir/b.png            b.texture.json + b.spriteframe.json            b.spriteframe.json                                                    ./dir/c.png            c.texture.json + c.spriteframe.json            c.spriteframe.json            c.spriteframe.json                                        ./dir/AutoAtlas            autoatlas.json            autoatlas.json            autoatlas.json                                        d.png            d.texture.json + d.spriteframe.json            d.spriteframe.json            d.spriteframe.json                                        d.plist            d.plist.json            d.plist.json            d.plist.json                                        e.prefab            e.prefab.json            e.prefab.json            e.prefab.json(pack a+b)                                        f.prefab            f.prefab.json            f.prefab.json            f.prefab.json(pack b)                                                                g.allTexture.json            g.allTexture.json            all.json        默认选项在绝大多数环境下都是一个不错的选择,假如是web平台,建议勾选
  1. 内联所有SpriteFrame
复制代码
这可以淘汰网络io,提高性能,而原平生台不建议勾选,这大概会增加包体巨细以及热更时要下载的内容。对于一些紧凑的Bundle(比如加载该Bundle就必要用到内里所有的资源),我们可以设置为归并所有的json。

2. 理解与利用 Asset Bundle


2.1 创建Bundle

Asset Bundle是creator 2.4之后的资源管理方案,简单地说就是通过目录来对资源进行规划,按照项目标需求将各种资源放到差别的目录下,并将目录设置成Asset Bundle。能够起到以下作用:
       
  • 加速游戏启动时间   
  • 减小首包体积   
  • 跨项目复用资源   
  • 方便实现子游戏   
  • 以Bundle为单元的热更新
Asset Bundle的创建非常简单,只要在目录的
  1. 属性检查器
复制代码
中勾选
  1. 配置为bundle
复制代码
即可,其中的选项官方文档都有比较详细的介绍。
其中关于压缩的理解,文档并没有详细的描述,这里的压缩指的并不是zip之类的压缩,而是通过packAssets的方式,把多个资源的json文件归并到一个,达到淘汰io的目标

在选项上打勾非常简单,真正的关键在于怎样规划Bundle,规划的原则在于淘汰包体、加速启动以及资源复用。根据游戏的模块来规划资源是比较不错的选择,比如按子游戏、关卡副本、或者系统功能来规划。
Bundle会主动将文件夹下的资源,以及文件夹中引用到的其它文件夹下的资源打包(假如这些资源不是在其它Bundle中),假如我们按照模块来规划资源,很容易出现多个Bundle共用了某个资源的环境。可以将公共资源提取到一个Bundle中,或者设置某个Bundle有较高的优先级,构建Bundle的依靠关系,否则这些资源会同时放到多个Bundle中(假如是本地Bundle,这会导致包体变大)。

2.2 利用Bundle

       
  • 关于加载资源 https://docs.cocos.com/creator/manual/zh/scripting/load-assets.html   
  • 关于释放资源 https://docs.cocos.com/creator/manual/zh/asset-manager/release-manager.html
Bundle的利用也非常简单,假如是resources目录下的资源,可以直接利用cc.resources.load来加载
  1. cc.resources.load("test assets/prefab", function (err, prefab) {
  2.     var newNode = cc.instantiate(prefab);
  3.     cc.director.getScene().addChild(newNode);
  4. });
复制代码
假如是其它自定义Bundle(本地Bundle或长途Bundle都可以用Bundle名加载),可以利用cc.assetManager.loadBundle来加载Bundle,然后利用加载后的Bundle对象,来加载Bundle中的资源。对于原平生台,假如Bundle被设置为长途包,在构建时必要在构建发布面板中填写资源服务器地址。
  1. cc.assetManager.loadBundle('01_graphics', (err, bundle) => {
  2.     bundle.load('xxx');
  3. });
复制代码
原生或小游戏平台下,我们还可以这样利用Bundle:
       
  • 假如要加载其它项目标长途Bundle,则必要利用url的方式加载(其它项目指另一个cocos工程)   
  • 假如希望自己管理Bundle的下载和缓存,可以放到本地可写路径,并传入路径来加载这些Bundle
  1. // 当复用其他项目的 Asset Bundle 时
  2. cc.assetManager.loadBundle('https://othergame.com/remote/01_graphics', (err, bundle) => {
  3.     bundle.load('xxx');
  4. });
  5. // 原生平台
  6. cc.assetManager.loadBundle(jsb.fileUtils.getWritablePath() + '/pathToBundle/bundleName', (err, bundle) => {
  7.     // ...
  8. });
  9. // 微信小游戏平台
  10. cc.assetManager.loadBundle(wx.env.USER_DATA_PATH + '/pathToBundle/bundleName', (err, bundle) => {
  11.     // ...
  12. });
复制代码
其它注意项:
       
  • 加载Bundle仅仅只是加载了Bundle的设置和脚本而已,Bundle中的其它资源还必要别的加载   
  • 目前原生的Bundle并不支持zip打包,长途包下载方式为逐文件下载,好处是操作简单,更新方便,弊端是io多,流量消耗大   
  • 差别Bundle下的脚本文件不要重名   
  • 一个Bundle A依靠另一个Bundle B,假如B没有被加载,加载A时并不会主动加载B,而是在加载A中依靠B的那个资源时报错

3. 新资源框架剖析

v2.4重构后的新框架代码更加简洁清楚,我们可以先从宏观角度相识一下整个资源框架,资源管线是整个框架最焦点的部分,它规范了整个资源加载的流程,并支持对管线进行自定义。
公共文件
       
  • helper.js 定义了一堆公共函数,如decodeUuid、getUuidFromURL、getUrlWithUuid等等   
  • utilities.js 定义了一堆公共函数,如getDepends、forEach、parseLoadResArgs等等   
  • deserialize.js 定义了deserialize方法,将json对象反序列化为Asset对象,并设置其
    1. __depends__
    复制代码
    属性   
  • depend-util.js 控制资源的依靠列表,每个资源的所有依靠都放在_depends成员变量中   
  • cache.js 通用缓存类,封装了一个浅易的键值对容器   
  • shared.js 定义了一些全局对象,主要是Cache和Pipeline对象,如加载好的assets、下载完的files以及bundles等
Bundle部分
       
  • config.js bundle的设置对象,负责解析bundle的config文件   
  • bundle.js bundle类,封装了config以及加载卸载bundle内资源的相干接口   
  • builtins.js 内建bundle资源的封装,可以通过
    1. cc.assetManager.builtins
    复制代码
    访问
管线部分
CCAssetManager.js 管理管线,提供同一的加载卸载接口
管线框架
       
  • pipeline.js 实现了管线的管道组合以及流转等根本功能   
  • task.js 定义了一个任务的根本属性,并提供了简单的任务池功能   
  • request-item.js 定义了一个资源下载项的根本属性,一个任务大概会天生多个下载项
预处理管线
       
  • urlTransformer.js parse将哀求参数转换成RequestItem对象(并查询相干的资源设置),combine负责转换真正的url   
  • preprocess.js 过滤出必要进行url转换的资源,并调用transformPipeline
下载管线
       
  • download-dom-audio.js 提供下载音效的方法,利用audio标签进行下载   
  • download-dom-image.js 提供下载图片的方法,利用Image标签进行下载   
  • download-file.js 提供下载文件的方法,利用XMLHttpRequest进行下载   
  • download-script.js 提供下载脚本的方法,利用script标签进行下载   
  • downloader.js 支持下载所有格式的下载器,支持并发控制、失败重试
解析管线
       
  • factory.js 创建Bundle、Asset、Texture2D等对象的工厂   
  • fetch.js 调用packManager下载资源,并解析依靠   
  • parser.js 对下载完成的文件进行解析
其它
       
  • releaseManager.js 提供资源释放接口、负责释放依靠资源以及场景切换时的资源释放   
  • cache-manager.d.ts 在非WEB平台上,用于管理所有从服务器上下载下来的缓存   
  • pack-manager.js 处理打包资源,包罗拆包,加载,缓存等等

3.1 加载管线

creator利用管线(pipeline)来处理整个资源加载的流程,这样的好处是解耦了资源处理的流程,将每一个步骤独立成一个单独的管道,管道可以很方便地进行复用和组合,并且方便了我们自定义整个加载流程,我们可以创建一些自己的管道,加入到管线中,比如资源加密。
AssetManager内置了3条管线,平凡的加载管线、预加载、以及资源路径转换管线,末了这条管线是为前面两条管线服务的。
  1. // 正常加载
  2. this.pipeline = pipeline.append(preprocess).append(load);
  3. // 预加载
  4. this.fetchPipeline = fetchPipeline.append(preprocess).append(fetch);
  5. // 转换资源路径
  6. this.transformPipeline = transformPipeline.append(parse).append(combine);
复制代码
3.1.1 启动加载管线【加载接口】
接下来我们看一下一个平凡的资源是怎样加载的,比如最简单的cc.resource.load,在bundle.load方法中,调用了cc.assetManager.loadAny,在loadAny方法中,创建了一个新的任务,并调用正常加载管线pipeline的async方法执行任务。

注意要加载的资源路径,被放到了task.input中、options是一个对象,对象包罗了type、bundle和__requestType__等字段
  1. // bundle类的load方法
  2. load (paths, type, onProgress, onComplete) {
  3.   var { type, onProgress, onComplete } = parseLoadResArgs(type, onProgress, onComplete);
  4.   cc.assetManager.loadAny(paths, { __requestType__: RequestType.PATH, type: type, bundle: this.name }, onProgress, onComplete);
  5. },
  6. // assetManager的loadAny方法
  7. loadAny (requests, options, onProgress, onComplete) {
  8.   var { options, onProgress, onComplete } = parseParameters(options, onProgress, onComplete);
  9.   
  10.   options.preset = options.preset || 'default';
  11.   let task = new Task({input: requests, onProgress, onComplete: asyncify(onComplete), options});
  12.   pipeline.async(task);
  13. },
复制代码
pipeline由两部分构成 preprocess 和 load。preprocess 由以下管线构成 preprocess、transformPipeline { parse、combine },preprocess实际上只创建了一个子任务,然后交由transformPipeline执行。对于加载一个平凡的资源,子任务的input和options与父任务相同。
  1. let subTask = Task.create({input: task.input, options: subOptions});
  2. task.output = task.source = transformPipeline.sync(subTask);
复制代码
3.1.2 transformPipeline管线【准备阶段】
transformPipeline由parse和combine两个管线构成,parse的职责是为每个要加载的资源天生RequestItem对象并初始化其资源信息(AssetInfo、uuid、config等):
先将input转换成数组进行遍历,假如是批量加载资源,每个加载项都会天生RequestItem
假如输入的item是object,则先将options拷贝到item身上(实际上每个item都会是object,假如是string的话,第一步就先转换成object了)
       
  • 对于UUID范例的item,先查抄bundle,并从bundle中提取AssetInfo,对于redirect范例的资源,则从其依靠的bundle中获取AssetInfo,找不到bundle就报错   
  • PATH范例和SCENE范例与UUID范例的处理根本类似,都是要拿到资源的详细信息   
  • DIR范例会从bundle中取出指定路径的信息,然后批量追加到input尾部(额外天生加载项)   
  • URL范例是长途资源范例,无需特别处理
  1. function parse (task) {
  2.     // 将input转换成数组
  3.     var input = task.input, options = task.options;
  4.     input = Array.isArray(input) ? input : [ input ];
  5.     task.output = [];
  6.     for (var i = 0; i < input.length; i ++ ) {
  7.         var item = input[i];
  8.         var out = RequestItem.create();
  9.         if (typeof item === 'string') {
  10.             // 先创建object
  11.             item = Object.create(null);
  12.             item[options.__requestType__ || RequestType.UUID] = input[i];
  13.         }
  14.         if (typeof item === 'object') {
  15.             // local options will overlap glabal options
  16.             // 将options的属性复制到item身上,addon会复制options上有,而item没有的属性
  17.             cc.js.addon(item, options);
  18.             if (item.preset) {
  19.                 cc.js.addon(item, cc.assetManager.presets[item.preset]);
  20.             }
  21.             for (var key in item) {
  22.                 switch (key) {
  23.                     // uuid类型资源,从bundle中取出该资源的详细信息
  24.                     case RequestType.UUID:
  25.                         var uuid = out.uuid = decodeUuid(item.uuid);
  26.                         if (bundles.has(item.bundle)) {
  27.                             var config = bundles.get(item.bundle)._config;
  28.                             var info = config.getAssetInfo(uuid);
  29.                             if (info && info.redirect) {
  30.                                 if (!bundles.has(info.redirect)) throw new Error(`Please load bundle ${info.redirect} first`);
  31.                                 config = bundles.get(info.redirect)._config;
  32.                                 info = config.getAssetInfo(uuid);
  33.                             }
  34.                             out.config = config;
  35.                             out.info = info;
  36.                         }
  37.                         out.ext = item.ext || '.json';
  38.                         break;
  39.                     case '__requestType__':
  40.                     case 'ext':
  41.                     case 'bundle':
  42.                     case 'preset':
  43.                     case 'type': break;
  44.                     case RequestType.DIR:
  45.                         // 解包后动态添加到input列表尾部,后续的循环会自动parse这些资源
  46.                         if (bundles.has(item.bundle)) {
  47.                             var infos = [];
  48.                             bundles.get(item.bundle)._config.getDirWithPath(item.dir, item.type, infos);
  49.                             for (let i = 0, l = infos.length; i < l; i++) {
  50.                                 var info = infos[i];
  51.                                 input.push({uuid: info.uuid, __isNative__: false, ext: '.json', bundle: item.bundle});
  52.                             }
  53.                         }
  54.                         out.recycle();
  55.                         out = null;
  56.                         break;
  57.                     case RequestType.PATH:
  58.                         // PATH类型的资源根据路径和type取出该资源的详细信息
  59.                         if (bundles.has(item.bundle)) {
  60.                             var config = bundles.get(item.bundle)._config;
  61.                             var info = config.getInfoWithPath(item.path, item.type);
  62.                            
  63.                             if (info && info.redirect) {
  64.                                 if (!bundles.has(info.redirect)) throw new Error(`you need to load bundle ${info.redirect} first`);
  65.                                 config = bundles.get(info.redirect)._config;
  66.                                 info = config.getAssetInfo(info.uuid);
  67.                             }
  68.                             if (!info) {
  69.                                 out.recycle();
  70.                                 throw new Error(`Bundle ${item.bundle} doesn't contain ${item.path}`);
  71.                             }
  72.                             out.config = config;
  73.                             out.uuid = info.uuid;
  74.                             out.info = info;
  75.                         }
  76.                         out.ext = item.ext || '.json';
  77.                         break;
  78.                     case RequestType.SCENE:
  79.                         // 场景类型,从bundle中的config调用getSceneInfo取出该场景的详细信息
  80.                         if (bundles.has(item.bundle)) {
  81.                             var config = bundles.get(item.bundle)._config;
  82.                             var info = config.getSceneInfo(item.scene);
  83.                            
  84.                             if (info && info.redirect) {
  85.                                 if (!bundles.has(info.redirect)) throw new Error(`you need to load bundle ${info.redirect} first`);
  86.                                 config = bundles.get(info.redirect)._config;
  87.                                 info = config.getAssetInfo(info.uuid);
  88.                             }
  89.                             if (!info) {
  90.                                 out.recycle();
  91.                                 throw new Error(`Bundle ${config.name} doesn't contain scene ${item.scene}`);
  92.                             }
  93.                             out.config = config;
  94.                             out.uuid = info.uuid;
  95.                             out.info = info;
  96.                         }
  97.                         break;
  98.                     case '__isNative__':
  99.                         out.isNative = item.__isNative__;
  100.                         break;
  101.                     case RequestType.URL:
  102.                         out.url = item.url;
  103.                         out.uuid = item.uuid || item.url;
  104.                         out.ext = item.ext || cc.path.extname(item.url);
  105.                         out.isNative = item.__isNative__ !== undefined ? item.__isNative__ : true;
  106.                         break;
  107.                     default: out.options[key] = item[key];
  108.                 }
  109.                 if (!out) break;
  110.             }
  111.         }
  112.         if (!out) continue;
  113.         task.output.push(out);
  114.         if (!out.uuid && !out.url) throw new Error('unknown input:' + item.toString());
  115.     }
  116.     return null;
  117. }
复制代码
RequestItem的初始信息,都是从bundle对象中查询的,bundle的信息则是从bundle自带的config.json文件中初始化的,在打包bundle的时候,会将bundle中的资源信息写入config.json中。
颠末parse方法处理后,我们会得到一系列RequestItem,并且很多RequestItem都自带了AssetInfo和uuid等信息,combine方法会为每个RequestItem构建出真正的加载路径,这个加载路径最终会转换到item.url中。
  1. function combine (task) {
  2.     var input = task.output = task.input;
  3.     for (var i = 0; i < input.length; i++) {
  4.         var item = input[i];
  5.         // 如果item已经包含了url,则跳过,直接使用item的url
  6.         if (item.url) continue;
  7.         var url = '', base = '';
  8.         var config = item.config;
  9.         // 决定目录的前缀
  10.         if (item.isNative) {
  11.             base = (config && config.nativeBase) ? (config.base + config.nativeBase) : cc.assetManager.generalNativeBase;
  12.         }
  13.         else {
  14.             base = (config && config.importBase) ? (config.base + config.importBase) : cc.assetManager.generalImportBase;
  15.         }
  16.         let uuid = item.uuid;
  17.             
  18.         var ver = '';
  19.         if (item.info) {
  20.             if (item.isNative) {
  21.                 ver = item.info.nativeVer ? ('.' + item.info.nativeVer) : '';
  22.             }
  23.             else {
  24.                 ver = item.info.ver ? ('.' + item.info.ver) : '';
  25.             }
  26.         }
  27.         // 拼接最终的url
  28.         // ugly hack, WeChat does not support loading font likes 'myfont.dw213.ttf'. So append hash to directory
  29.         if (item.ext === '.ttf') {
  30.             url = `${base}/${uuid.slice(0, 2)}/${uuid}${ver}/${item.options.__nativeName__}`;
  31.         }
  32.         else {
  33.             url = `${base}/${uuid.slice(0, 2)}/${uuid}${ver}${item.ext}`;
  34.         }
  35.         
  36.         item.url = url;
  37.     }
  38.     return null;
  39. }
复制代码
3.1.3 load管线【加载流程】

load方法做的事故很简单,根本只是创建了新的任务,在loadOneAssetPipeline中执行每个子任务
  1. function load (task, done) {
  2.     if (!task.progress) {
  3.         task.progress = {finish: 0, total: task.input.length};
  4.     }
  5.    
  6.     var options = task.options, progress = task.progress;
  7.     options.__exclude__ = options.__exclude__ || Object.create(null);
  8.     task.output = [];
  9.     forEach(task.input, function (item, cb) {
  10.         // 对每个input项都创建一个子任务,并交由loadOneAssetPipeline执行
  11.         let subTask = Task.create({
  12.             input: item,
  13.             onProgress: task.onProgress,
  14.             options,
  15.             progress,
  16.             onComplete: function (err, item) {
  17.                 if (err && !task.isFinish && !cc.assetManager.force) done(err);
  18.                 task.output.push(item);
  19.                 subTask.recycle();
  20.                 cb();
  21.             }
  22.         });
  23.         // 执行子任务,loadOneAssetPipeline有fetch和parse组成
  24.         loadOneAssetPipeline.async(subTask);
  25.     }, function () {
  26.         // 每个input执行完成后,最后执行该函数
  27.         options.__exclude__ = null;
  28.         if (task.isFinish) {
  29.             clear(task, true);
  30.             return task.dispatch('error');
  31.         }
  32.         gatherAsset(task);
  33.         clear(task, true);
  34.         done();
  35.     });
  36. }
复制代码
loadOneAssetPipeline如其函数名所示,就是加载一个资源的管线,它分为2步,fetch和parse:
fetch方法用于下载资源文件,由packManager负责下载的实现,fetch会将下载完的文件数据放到item.file中
parse方法用于将加载完的资源文件转换成我们可用的资源对象
对于原生资源,调用parser.parse进行解析,该方法会根据资源范例调用差别的解析方法
       
  • import资源调用parseImport方法,根据json数据反序列化出Asset对象,并放到assets中   
  • 图片资源会调用parseImage、parsePVRTex或parsePKMTex方法解析图像格式(但不会创建Texture对象)   
  • 音效资源调用parseAudio方法进行解析   
  • plist资源调用parsePlist方法进行解析
对于其它资源
假如uuid在
  1. task.options.__exclude__
复制代码
中,则标记为完成,并添加引用计数,否则,根据一些复杂的条件来决定是否加载资源的依靠
  1. var loadOneAssetPipeline = new Pipeline('loadOneAsset', [
  2.     function fetch (task, done) {
  3.         var item = task.output = task.input;
  4.         var { options, isNative, uuid, file } = item;
  5.         var { reload } = options;
  6.         // 如果assets里面已经加载了这个资源,则直接完成
  7.         if (file || (!reload && !isNative && assets.has(uuid))) return done();
  8.         // 下载文件,这是一个异步的过程,文件下载完会被放到item.file中,并执行done驱动管线
  9.         packManager.load(item, task.options, function (err, data) {
  10.             if (err) {
  11.                 if (cc.assetManager.force) {
  12.                     err = null;
  13.                 } else {
  14.                     cc.error(err.message, err.stack);
  15.                 }
  16.                 data = null;
  17.             }
  18.             item.file = data;
  19.             done(err);
  20.         });
  21.     },
  22.     // 将资源文件转换成资源对象的过程
  23.     function parse (task, done) {
  24.         var item = task.output = task.input, progress = task.progress, exclude = task.options.__exclude__;
  25.         var { id, file, options } = item;
  26.         if (item.isNative) {
  27.             // 对于原生资源,调用parser.parse进行处理,将处理完的资源放到item.content中,并结束流程
  28.             parser.parse(id, file, item.ext, options, function (err, asset) {
  29.                 if (err) {
  30.                     if (!cc.assetManager.force) {
  31.                         cc.error(err.message, err.stack);
  32.                         return done(err);
  33.                     }
  34.                 }
  35.                 item.content = asset;
  36.                 task.dispatch('progress', ++progress.finish, progress.total, item);
  37.                 files.remove(id);
  38.                 parsed.remove(id);
  39.                 done();
  40.             });
  41.         } else {
  42.             var { uuid } = item;
  43.             // 非原生资源,如果在task.options.__exclude__中,直接结束
  44.             if (uuid in exclude) {
  45.                 var { finish, content, err, callbacks } = exclude[uuid];
  46.                 task.dispatch('progress', ++progress.finish, progress.total, item);
  47.    
  48.                 if (finish || checkCircleReference(uuid, uuid, exclude) ) {
  49.                     content && content.addRef();
  50.                     item.content = content;
  51.                     done(err);
  52.                 } else {
  53.                     callbacks.push({ done, item });
  54.                 }
  55.             } else {
  56.                 // 如果不是reload,且asset中包含了该uuid
  57.                 if (!options.reload && assets.has(uuid)) {
  58.                     var asset = assets.get(uuid);
  59.                     // 开启了options.__asyncLoadAssets__,或asset.__asyncLoadAssets__为false,直接结束,不加载依赖
  60.                     if (options.__asyncLoadAssets__ || !asset.__asyncLoadAssets__) {
  61.                         item.content = asset.addRef();
  62.                         task.dispatch('progress', ++progress.finish, progress.total, item);
  63.                         done();
  64.                     }
  65.                     else {
  66.                         loadDepends(task, asset, done, false);
  67.                     }
  68.                 } else {
  69.                     // 如果是reload,或者assets中没有,则进行解析,并加载依赖
  70.                     parser.parse(id, file, 'import', options, function (err, asset) {
  71.                         if (err) {
  72.                             if (cc.assetManager.force) {
  73.                                 err = null;
  74.                             }
  75.                             else {
  76.                                 cc.error(err.message, err.stack);
  77.                             }
  78.                             return done(err);
  79.                         }
  80.                         
  81.                         asset._uuid = uuid;
  82.                         loadDepends(task, asset, done, true);
  83.                     });
  84.                 }
  85.             }
  86.         }
  87.     }
  88. ]);
复制代码
3.2 文件下载

creator利用
  1. packManager.load
复制代码
来完成下载的工作,当要下载一个文件时,有2个题目必要思量:
       
  • 该文件是否被打包了,比如由于勾选了内联所有SpriteFrame,导致SpriteFrame的json文件被归并到prefab中   
  • 当前平台是原平生台还是web平台,对于一些本地资源,原平生台必要从磁盘读取
  1. // packManager.load的实现
  2. load (item, options, onComplete) {
  3.   // 如果资源没有被打包,则直接调用downloader.download下载(download内部也有已下载和加载中的判断)
  4.   if (item.isNative || !item.info || !item.info.packs) return downloader.download(item.id, item.url, item.ext, item.options, onComplete);
  5.   // 如果文件已经下载过了,则直接返回
  6.   if (files.has(item.id)) return onComplete(null, files.get(item.id));
  7.   var packs = item.info.packs;
  8.   // 如果pack已经在加载中,则将回调添加到_loading队列,等加载完成后触发回调
  9.   var pack = packs.find(isLoading);
  10.   if (pack) return _loading.get(pack.uuid).push({ onComplete, id: item.id });
  11.   // 下载一个新的pack
  12.   pack = packs[0];
  13.   _loading.add(pack.uuid, [{ onComplete, id: item.id }]);
  14.   let url = cc.assetManager._transform(pack.uuid, {ext: pack.ext, bundle: item.config.name});
  15.   // 下载pack并解包,
  16.   downloader.download(pack.uuid, url, pack.ext, item.options, function (err, data) {
  17.       files.remove(pack.uuid);
  18.       if (err) {
  19.           cc.error(err.message, err.stack);
  20.       }
  21.       // unpack package,内部实现包含2种解包,一种针对prefab、图集等json数组的分割解包,另一种针对Texture2D的content进行解包
  22.       packManager.unpack(pack.packs, data, pack.ext, item.options, function (err, result) {
  23.           if (!err) {
  24.               for (var id in result) {
  25.                   files.add(id, result[id]);
  26.               }
  27.           }
  28.           var callbacks = _loading.remove(pack.uuid);
  29.           for (var i = 0, l = callbacks.length; i < l; i++) {
  30.               var cb = callbacks[i];
  31.               if (err) {
  32.                   cb.onComplete(err);
  33.                   continue;
  34.               }
  35.               var data = result[cb.id];
  36.               if (!data) {
  37.                   cb.onComplete(new Error('can not retrieve data from package'));
  38.               }
  39.               else {
  40.                   cb.onComplete(null, data);
  41.               }
  42.           }
  43.       });
  44.   });
  45. }
复制代码
3.2.1 Web平台的下载
web平台的download实现如下:
       
  • 用一个downloaders数组来管理各种资源范例对应的下载方式   
  • 利用files缓存来克制重复下载   
  • 利用_downloading队列来处理并发下载同一个资源时的回调,并保证时序   
  • 支持了下载的优先级、重试等逻辑
  1. download (id, url, type, options, onComplete) {
  2.   // 取出downloaders中对应类型的下载回调
  3.   let func = downloaders[type] || downloaders['default'];
  4.   let self = this;
  5.   // 避免重复下载
  6.   let file, downloadCallbacks;
  7.   if (file = files.get(id)) {
  8.       onComplete(null, file);
  9.   }
  10.   // 如果在下载中,添加到队列
  11.   else if (downloadCallbacks = _downloading.get(id)) {
  12.       downloadCallbacks.push(onComplete);
  13.       for (let i = 0, l = _queue.length; i < l; i++) {
  14.           var item = _queue[i];
  15.           if (item.id === id) {
  16.               var priority = options.priority || 0;
  17.               if (item.priority < priority) {
  18.                   item.priority = priority;
  19.                   _queueDirty = true;
  20.               }
  21.               return;
  22.           }
  23.       }
  24.   }
  25.   else {
  26.       // 进行下载,并设置好下载失败的重试
  27.       var maxRetryCount = options.maxRetryCount || this.maxRetryCount;
  28.       var maxConcurrency = options.maxConcurrency || this.maxConcurrency;
  29.       var maxRequestsPerFrame = options.maxRequestsPerFrame || this.maxRequestsPerFrame;
  30.       function process (index, callback) {
  31.           if (index === 0) {
  32.               _downloading.add(id, [onComplete]);
  33.           }
  34.           if (!self.limited) return func(urlAppendTimestamp(url), options, callback);
  35.           updateTime();
  36.           function invoke () {
  37.               func(urlAppendTimestamp(url), options, function () {
  38.                   // when finish downloading, update _totalNum
  39.                   _totalNum--;
  40.                   if (!_checkNextPeriod && _queue.length > 0) {
  41.                       callInNextTick(handleQueue, maxConcurrency, maxRequestsPerFrame);
  42.                       _checkNextPeriod = true;
  43.                   }
  44.                   callback.apply(this, arguments);
  45.               });
  46.           }
  47.           if (_totalNum < maxConcurrency && _totalNumThisPeriod < maxRequestsPerFrame) {
  48.               invoke();
  49.               _totalNum++;
  50.               _totalNumThisPeriod++;
  51.           }
  52.           else {
  53.               // when number of request up to limitation, cache the rest
  54.               _queue.push({ id, priority: options.priority || 0, invoke });
  55.               _queueDirty = true;
  56.               if (!_checkNextPeriod && _totalNum < maxConcurrency) {
  57.                   callInNextTick(handleQueue, maxConcurrency, maxRequestsPerFrame);
  58.                   _checkNextPeriod = true;
  59.               }
  60.           }
  61.       }
  62.       // retry完成后,将文件添加到files缓存中,从_downloading队列中移除,并执行callbacks回调
  63.       // when retry finished, invoke callbacks
  64.       function finale (err, result) {
  65.           if (!err) files.add(id, result);
  66.           var callbacks = _downloading.remove(id);
  67.           for (let i = 0, l = callbacks.length; i < l; i++) {
  68.               callbacks[i](err, result);
  69.           }
  70.       }
  71.       retry(process, maxRetryCount, this.retryInterval, finale);
  72.   }
  73. }
复制代码
downloaders是一个map,映射了各种资源范例对应的下载方法,在web平台主要包罗以下几类下载方法:
图片类 downloadImage
       
  • downloadDomImage 利用Html的Image元素,指定其src属性来下载   
  • downloadBlob 以文件下载的方式下载图片
文件类,这里可以分为二进制文件、json文件和文本文件
       
  • downloadArrayBuffer 指定arraybuffer范例调用downloadFile,用于skel、bin、pvr等文件下载   
  • downloadText 指定text范例调用downloadFile,用于atlas、tmx、xml、vsh等文件下载   
  • downloadJson 指定json范例调用downloadFile,并在下载完后解析json,用于plist、json等文件下载
字体类 loadFont 构建css样式,指定url下载
声音类 downloadAudio
       
  • downloadDomAudio 创建Html的audio元素,指定其src属性来下载   
  • downloadBlob 以文件下载的方式下载音效
视频类 downloadVideo web端直接返回了
脚本 downloadScript 创建Html的script元素,指定其src属性来下载并执行
Bundle downloadBundle 同时下载了Bundle的json和脚本
downloadFile利用了XMLHttpRequest来下载文件,详细实现如下:
  1. function downloadFile (url, options, onProgress, onComplete) {
  2.     var { options, onProgress, onComplete } = parseParameters(options, onProgress, onComplete);
  3.     var xhr = new XMLHttpRequest(), errInfo = 'download failed: ' + url + ', status: ';
  4.     xhr.open('GET', url, true);
  5.    
  6.     if (options.responseType !== undefined) xhr.responseType = options.responseType;
  7.     if (options.withCredentials !== undefined) xhr.withCredentials = options.withCredentials;
  8.     if (options.mimeType !== undefined && xhr.overrideMimeType ) xhr.overrideMimeType(options.mimeType);
  9.     if (options.timeout !== undefined) xhr.timeout = options.timeout;
  10.     if (options.header) {
  11.         for (var header in options.header) {
  12.             xhr.setRequestHeader(header, options.header[header]);
  13.         }
  14.     }
  15.     xhr.onload = function () {
  16.         if ( xhr.status === 200 || xhr.status === 0 ) {
  17.             onComplete && onComplete(null, xhr.response);
  18.         } else {
  19.             onComplete && onComplete(new Error(errInfo + xhr.status + '(no response)'));
  20.         }
  21.     };
  22.     if (onProgress) {
  23.         xhr.onprogress = function (e) {
  24.             if (e.lengthComputable) {
  25.                 onProgress(e.loaded, e.total);
  26.             }
  27.         };
  28.     }
  29.     xhr.onerror = function(){
  30.         onComplete && onComplete(new Error(errInfo + xhr.status + '(error)'));
  31.     };
  32.     xhr.ontimeout = function(){
  33.         onComplete && onComplete(new Error(errInfo + xhr.status + '(time out)'));
  34.     };
  35.     xhr.onabort = function(){
  36.         onComplete && onComplete(new Error(errInfo + xhr.status + '(abort)'));
  37.     };
  38.     xhr.send(null);
  39.     return xhr;
  40. }
复制代码
3.2.2 原平生台下载
原平生台的引擎相干文件可以在引擎目录的
  1. resources/builtin/jsb-adapter/engine
复制代码
目录下,资源加载相干的实现在jsb-loader.js文件中,这里的downloader重新注册了回调函数。
  1. downloader.register({
  2.     // JS
  3.     '.js' : downloadScript,
  4.     '.jsc' : downloadScript,
  5.     // Images
  6.     '.png' : downloadAsset,
  7.     '.jpg' : downloadAsset,
  8.     ...
  9. });
复制代码
在原平生台下,downloadAsset等方法都会调用download来进行资源的下载,在资源下载之前会调用transformUrl对url进行检测,主要判断该资源是网络资源还是本地资源,假如是网络资源,是否已经下载过了。只有没下载过的网络资源,才必要进行下载。不必要下载的在文件解析的地方会直接读文件。
  1. // func传入的是下载完成之后的处理,比如脚本下载完成后需要执行,此时会调用window.require
  2. // 如果说要下载的是json资源之类的,传入的func是doNothing,也就是直接调用onComplete方法
  3. function download (url, func, options, onFileProgress, onComplete) {
  4.     var result = transformUrl(url, options);
  5.     // 如果是本地文件,直接指向func
  6.     if (result.inLocal) {
  7.         func(result.url, options, onComplete);
  8.     }
  9.     // 如果在缓存中,更新资源的最后使用时间(lru)
  10.     else if (result.inCache) {
  11.         cacheManager.updateLastTime(url)
  12.         func(result.url, options, function (err, data) {
  13.             if (err) {
  14.                 cacheManager.removeCache(url);
  15.             }
  16.             onComplete(err, data);
  17.         });
  18.     }
  19.     else {
  20.         // 未下载的网络资源,调用downloadFile进行下载
  21.         var time = Date.now();
  22.         var storagePath = '';
  23.         if (options.__cacheBundleRoot__) {
  24.             storagePath = `${cacheManager.cacheDir}/${options.__cacheBundleRoot__}/${time}${suffix++}${cc.path.extname(url)}`;
  25.         }
  26.         else {
  27.             storagePath = `${cacheManager.cacheDir}/${time}${suffix++}${cc.path.extname(url)}`;
  28.         }
  29.         // 使用downloadFile下载并缓存
  30.         downloadFile(url, storagePath, options.header, onFileProgress, function (err, path) {
  31.             if (err) {
  32.                 onComplete(err, null);
  33.                 return;
  34.             }
  35.             func(path, options, function (err, data) {
  36.                 if (!err) {
  37.                     cacheManager.cacheFile(url, storagePath, options.__cacheBundleRoot__);
  38.                 }
  39.                 onComplete(err, data);
  40.             });
  41.         });
  42.     }
  43. }
  44. function transformUrl (url, options) {
  45.     var inLocal = false;
  46.     var inCache = false;
  47.     // 通过正则匹配是不是URL
  48.     if (REGEX.test(url)) {
  49.         if (options.reload) {
  50.             return { url };
  51.         }
  52.         else {
  53.             // 检查是否在缓存中(本地磁盘缓存)
  54.             var cache = cacheManager.cachedFiles.get(url);
  55.             if (cache) {
  56.                 inCache = true;
  57.                 url = cache.url;
  58.             }
  59.         }
  60.     }
  61.     else {
  62.         inLocal = true;
  63.     }
  64.     return { url, inLocal, inCache };
  65. }
复制代码
downloadFile会调用原平生台的jsb_downloader来下载资源,并生存到本地磁盘中
  1. downloadFile (remoteUrl, filePath, header, onProgress, onComplete) {
  2.   downloading.add(remoteUrl, { onProgress, onComplete });
  3.   var storagePath = filePath;
  4.   if (!storagePath) storagePath = tempDir + '/' + performance.now() + cc.path.extname(remoteUrl);
  5.   jsb_downloader.createDownloadFileTask(remoteUrl, storagePath, header);
  6. },
复制代码
3.3 文件解析

在loadOneAssetPipeline中,资源会颠末fetch和parse两个管线进行处理,fetch负责下载而parse负责解析资源,并实例化资源对象。在parse方法中调用了parser.parse将文件内容传入,解析成对应的Asset对象,并返回。
3.3.1 Web平台解析
Web平台下的parser.parse主要做的是对解析中的文件的管理,为解析中、解析完的文件维护一个列表,克制重复解析。同时维护相识析完成后的回调列表,而真正的解析方法在parsers数组中。
  1. parse (id, file, type, options, onComplete) {
  2.   let parsedAsset, parsing, parseHandler;
  3.   if (parsedAsset = parsed.get(id)) {
  4.       onComplete(null, parsedAsset);
  5.   }
  6.   else if (parsing = _parsing.get(id)){
  7.       parsing.push(onComplete);
  8.   }
  9.   else if (parseHandler = parsers[type]){
  10.       _parsing.add(id, [onComplete]);
  11.       parseHandler(file, options, function (err, data) {
  12.           if (err) {
  13.               files.remove(id);
  14.           }
  15.           else if (!isScene(data)){
  16.               parsed.add(id, data);
  17.           }
  18.           let callbacks = _parsing.remove(id);
  19.           for (let i = 0, l = callbacks.length; i < l; i++) {
  20.               callbacks[i](err, data);
  21.           }
  22.       });
  23.   }
  24.   else {
  25.       onComplete(null, file);
  26.   }
  27. }
复制代码
parsers映射了各种范例文件的解析方法,下面以图片和平凡的asset资源为例:
注意:在parseImport方法中,反序列化方法会将资源的依靠放到asset.__depends__中,布局为数组,数组中每个对象包罗3个字段,资源id uuid、owner 对象、prop 属性。比如一个Prefab资源,下面有2个节点,都引用了同一个资源,depends列表必要为这两个节点对象分别记录一条依靠信息 [{uuid:xxx, owner:1, prop:tex}, {uuid:xxx, owner:2, prop:tex}]
  1. // 映射图片格式到解析方法
  2. var parsers = {
  3.   '.png' : parser.parseImage,
  4.   '.jpg' : parser.parseImage,
  5.   '.bmp' : parser.parseImage,
  6.   '.jpeg' : parser.parseImage,
  7.   '.gif' : parser.parseImage,
  8.   '.ico' : parser.parseImage,
  9.   '.tiff' : parser.parseImage,
  10.   '.webp' : parser.parseImage,
  11.   '.image' : parser.parseImage,
  12.   '.pvr' : parser.parsePVRTex,
  13.   '.pkm' : parser.parsePKMTex,
  14.   // Audio
  15.   '.mp3' : parser.parseAudio,
  16.   '.ogg' : parser.parseAudio,
  17.   '.wav' : parser.parseAudio,
  18.   '.m4a' : parser.parseAudio,
  19.   // plist
  20.   '.plist' : parser.parsePlist,
  21.   'import' : parser.parseImport
  22. };
  23. // 图片并不会解析成Asset对象,而是解析成对应的图片对象
  24. parseImage (file, options, onComplete) {
  25.   if (capabilities.imageBitmap && file instanceof Blob) {
  26.       let imageOptions = {};
  27.       imageOptions.imageOrientation = options.__flipY__ ? 'flipY' : 'none';
  28.       imageOptions.premultiplyAlpha = options.__premultiplyAlpha__ ? 'premultiply' : 'none';
  29.       createImageBitmap(file, imageOptions).then(function (result) {
  30.           result.flipY = !!options.__flipY__;
  31.           result.premultiplyAlpha = !!options.__premultiplyAlpha__;
  32.           onComplete && onComplete(null, result);
  33.       }, function (err) {
  34.           onComplete && onComplete(err, null);
  35.       });
  36.   }
  37.   else {
  38.       onComplete && onComplete(null, file);
  39.   }
  40. },
  41. // Asset对象的解析,通过deserialize实现,大致流程是解析json然后找到对应的class,并调用对应class的_deserialize方法拷贝数据、初始化变量,并将依赖资源放到asset.__depends
  42. parseImport (file, options, onComplete) {
  43.   if (!file) return onComplete && onComplete(new Error('Json is empty'));
  44.   var result, err = null;
  45.   try {
  46.       result = deserialize(file, options);
  47.   }
  48.   catch (e) {
  49.       err = e;
  50.   }
  51.   onComplete && onComplete(err, result);
  52. },
复制代码
3.3.2 原平生台解析
在原平生台下,jsb-loader.js中重新注册了各种资源的解析方法:
  1. parser.register({
  2.     '.png' : downloader.downloadDomImage,
  3.     '.binary' : parseArrayBuffer,
  4.     '.txt' : parseText,
  5.     '.plist' : parsePlist,
  6.     '.font' : loadFont,
  7.     '.ExportJson' : parseJson,
  8.     ...
  9. });
复制代码
图片的解析方法竟然是downloader.downloadDomImage?跟踪原平生台调试了一下,确实是调用的这个方法,创建了Image对象并指定src来加载图片,这种方式加载本地磁盘的图片也是可以的,但纹理对象又是怎样创建的呢?通过Texture2D对应的json文件,creator在加载真正的原生纹理之前,就已经创建好了Texture2D这个Asset对象,而在加载完原生图片资源后,会将Image对象设置为Texture2D对象的_nativeAsset,在这个属性的set方法中,会调用initWithData或initWithElement,这里才真正利用纹理数据创建了用于渲染的纹理对象。
  1. var Texture2D = cc.Class({
  2.     name: 'cc.Texture2D',
  3.     extends: require('../assets/CCAsset'),
  4.     mixins: [EventTarget],
  5.     properties: {
  6.         _nativeAsset: {
  7.             get () {
  8.                 // maybe returned to pool in webgl
  9.                 return this._image;
  10.             },
  11.             set (data) {
  12.                 if (data._data) {
  13.                     this.initWithData(data._data, this._format, data.width, data.height);
  14.                 }
  15.                 else {
  16.                     this.initWithElement(data);
  17.                 }
  18.             },
  19.             override: true
  20.         },
复制代码
而对于parseJson、parseText、parseArrayBuffer等实现,这里只是简单地调用了文件系统读取文件而已。像一些拿到文件内容之后,必要进一步解析才气利用的资源呢?比如模子、骨骼等资源依靠二进制的模子数据,这些数据的解析在哪里呢?没错,跟上面的Texture2D一样,都是放在对应的Asset资源自己,有些在_nativeAsset字段的setter回调中初始化,而有些会在真正利用这个资源时才惰性地进行初始化。
  1. // 在jsb-loader.js文件中
  2. function parseText (url, options, onComplete) {
  3.     readText(url, onComplete);
  4. }
  5. function parseArrayBuffer (url, options, onComplete) {
  6.     readArrayBuffer(url, onComplete);
  7. }
  8. function parseJson (url, options, onComplete) {
  9.     readJson(url, onComplete);
  10. }
  11. // 在jsb-fs-utils.js文件中
  12.     readText (filePath, onComplete) {
  13.         fsUtils.readFile(filePath, 'utf8', onComplete);
  14.     },
  15.     readArrayBuffer (filePath, onComplete) {
  16.         fsUtils.readFile(filePath, '', onComplete);
  17.     },
  18.     readJson (filePath, onComplete) {
  19.         fsUtils.readFile(filePath, 'utf8', function (err, text) {
  20.             var out = null;
  21.             if (!err) {
  22.                 try {
  23.                     out = JSON.parse(text);
  24.                 }
  25.                 catch (e) {
  26.                     cc.warn('Read json failed: ' + e.message);
  27.                     err = new Error(e.message);
  28.                 }
  29.             }
  30.             onComplete && onComplete(err, out);
  31.         });
  32.     },
复制代码
像图集、Prefab这些资源又是怎么初始化的呢?Creator还是利用parseImport方法进行解析,由于这些资源对应的范例是
  1. import
复制代码
,原平生台下并没有覆盖这种范例对应的parse函数,而这些资源会直接反序列化成可用的Asset对象。

3.4 依靠加载

creator将资源分为两大类,平凡资源和原生资源,平凡资源包罗cc.Asset及其子类,如cc.SpriteFrame、cc.Texture2D、cc.Prefab等等。原生资源包罗各种格式的纹理、音乐、字体等文件,在游戏中我们无法直接利用这些原生资源,而是必要让creator将他们转换成对应的cc.Asset对象之后才气利用。
在creator中,一个Prefab大概会依靠很多资源,这些依靠也可以分为平凡依靠和原生资源依靠,creator的cc.Asset提供了
  1. _parseDepsFromJson
复制代码
  1. _parseNativeDepFromJson
复制代码
方法来查抄资源的依靠。loadDepends通过getDepends方法搜集了资源的依靠。
loadDepends创建了一个子任务来负责依靠资源的加载,并调用pipeline执行加载,实际上无论有无依靠必要加载,都会执行这段逻辑,加载完成后执行以下紧张逻辑:
       
  • 初始化assset:在依靠加载完成后,将依靠的资源赋值到asset对应的属性后调用asset.onLoad   
  • 将资源对应的files和parsed缓存移除,并缓存资源到assets中(假如是场景的话,不会缓存)   
  • 执行repeatItem.callbacks列表中的回调(在loadDepends的开头构造,默认记录传入的done方法)
  1. // 加载指定asset的依赖项
  2. function loadDepends (task, asset, done, init) {
  3.     var item = task.input, progress = task.progress;
  4.     var { uuid, id, options, config } = item;
  5.     var { __asyncLoadAssets__, cacheAsset } = options;
  6.     var depends = [];
  7.     // 增加引用计数来避免加载依赖的过程中资源被释放,调用getDepends获取依赖资源
  8.     asset.addRef && asset.addRef();
  9.     getDepends(uuid, asset, Object.create(null), depends, false, __asyncLoadAssets__, config);
  10.     task.dispatch('progress', ++progress.finish, progress.total += depends.length, item);
  11.     var repeatItem = task.options.__exclude__[uuid] = { content: asset, finish: false, callbacks: [{ done, item }] };
  12.     let subTask = Task.create({
  13.         input: depends,
  14.         options: task.options,
  15.         onProgress: task.onProgress,
  16.         onError: Task.prototype.recycle,
  17.         progress,
  18.         onComplete: function (err) {
  19.             // 在所有依赖项加载完成之后回调
  20.             asset.decRef && asset.decRef(false);
  21.             asset.__asyncLoadAssets__ = __asyncLoadAssets__;
  22.             repeatItem.finish = true;
  23.             repeatItem.err = err;
  24.             if (!err) {
  25.                 var assets = Array.isArray(subTask.output) ? subTask.output : [subTask.output];
  26.                 // 构造一个map,记录uuid到asset的映射
  27.                 var map = Object.create(null);
  28.                 for (let i = 0, l = assets.length; i < l; i++) {
  29.                     var dependAsset = assets[i];
  30.                     dependAsset && (map[dependAsset instanceof cc.Asset ? dependAsset._uuid + '@import' : uuid + '@native'] = dependAsset);
  31.                 }
  32.                 // 调用setProperties将对应的依赖资源设置到asset的成员变量中
  33.                 if (!init) {
  34.                     if (asset.__nativeDepend__ && !asset._nativeAsset) {
  35.                         var missingAsset = setProperties(uuid, asset, map);
  36.                         if (!missingAsset) {
  37.                             try {
  38.                                 asset.onLoad && asset.onLoad();
  39.                             }
  40.                             catch (e) {
  41.                                 cc.error(e.message, e.stack);
  42.                             }
  43.                         }
  44.                     }
  45.                 }
  46.                 else {
  47.                     var missingAsset = setProperties(uuid, asset, map);
  48.                     if (!missingAsset) {
  49.                         try {
  50.                             asset.onLoad && asset.onLoad();
  51.                         }
  52.                         catch (e) {
  53.                             cc.error(e.message, e.stack);
  54.                         }
  55.                     }
  56.                     files.remove(id);
  57.                     parsed.remove(id);
  58.                     cache(uuid, asset, cacheAsset !== undefined ? cacheAsset : cc.assetManager.cacheAsset);
  59.                 }
  60.                 subTask.recycle();
  61.             }
  62.             
  63.             // 这个repeatItem可能有很多个地方都加载了它,要通知所有回调加载完成
  64.             var callbacks = repeatItem.callbacks;
  65.             for (var i = 0, l = callbacks.length; i < l; i++) {
  66.                 var cb = callbacks[i];
  67.                 asset.addRef && asset.addRef();
  68.                 cb.item.content = asset;
  69.                 cb.done(err);
  70.             }
  71.             callbacks.length = 0;
  72.         }
  73.     });
  74.     pipeline.async(subTask);
  75. }
复制代码
3.4.1 依靠解析
  1. getDepends (uuid, data, exclude, depends, preload, asyncLoadAssets, config) {
  2.   var err = null;
  3.   try {
  4.       var info = dependUtil.parse(uuid, data);
  5.       var includeNative = true;
  6.       if (data instanceof cc.Asset && (!data.__nativeDepend__ || data._nativeAsset)) includeNative = false;
  7.       if (!preload) {
  8.           asyncLoadAssets = !CC_EDITOR && (!!data.asyncLoadAssets || (asyncLoadAssets && !info.preventDeferredLoadDependents));
  9.           for (let i = 0, l = info.deps.length; i < l; i++) {
  10.               let dep = info.deps[i];
  11.               if (!(dep in exclude)) {
  12.                   exclude[dep] = true;
  13.                   depends.push({uuid: dep, __asyncLoadAssets__: asyncLoadAssets, bundle: config && config.name});
  14.               }
  15.           }
  16.           if (includeNative && !asyncLoadAssets && !info.preventPreloadNativeObject && info.nativeDep) {
  17.               config && (info.nativeDep.bundle = config.name);
  18.               depends.push(info.nativeDep);
  19.           }
  20.          
  21.       } else {
  22.           for (let i = 0, l = info.deps.length; i < l; i++) {
  23.               let dep = info.deps[i];
  24.               if (!(dep in exclude)) {
  25.                   exclude[dep] = true;
  26.                   depends.push({uuid: dep, bundle: config && config.name});
  27.               }
  28.           }
  29.           if (includeNative && info.nativeDep) {
  30.               config && (info.nativeDep.bundle = config.name);
  31.               depends.push(info.nativeDep);
  32.           }
  33.       }
  34.   }
  35.   catch (e) {
  36.       err = e;
  37.   }
  38.   return err;
  39. },
复制代码
dependUtil是一个控制依靠列表的单例,通过传入uuid和asset对象来解析该对象的依靠资源列表,返回的依靠资源列表大概包罗以下4个字段:
       
  • deps 依靠的Asset资源   
  • nativeDep 依靠的原生资源   
  • preventPreloadNativeObject 克制预加载原生对象,这个值默认是false   
  • preventDeferredLoadDependents 克制延迟加载依靠,默以为false,对于骨骼动画、TiledMap等资源为true   
  • parsedFromExistAsset 是否直接从
    1. asset.__depends__
    复制代码
    中取出
dependUtil还维护了_depends缓存来克制依靠的重复查询,这个缓存会在初次查询某资源依靠时添加,当该资源被释放时移除
  1. // 根据json信息获取其资源依赖列表,实际上json信息就是asset对象
  2. parse (uuid, json) {
  3.   var out = null;
  4.   // 如果是场景或者Prefab,data会是一个数组,scene or prefab
  5.   if (Array.isArray(json)) {
  6.       // 如果已经解析过了,在_depends中有依赖列表,则直接返回
  7.       if (this._depends.has(uuid)) return this._depends.get(uuid)
  8.       out = {
  9.           // 对于Prefab或场景,直接使用_parseDepsFromJson方法返回
  10.           deps: cc.Asset._parseDepsFromJson(json),
  11.           asyncLoadAssets: json[0].asyncLoadAssets
  12.       };
  13.   }
  14.   // 如果包含__type__,获取其构造函数,并从json中查找依赖资源 get deps from json
  15.   // 实际测试,预加载的资源会走下面这个分支,预加载的资源并没有把json反序列化成Asset对象
  16.   else if (json.__type__) {
  17.       if (this._depends.has(uuid)) return this._depends.get(uuid);
  18.       var ctor = js._getClassById(json.__type__);
  19.       // 部分资源重写了_parseDepsFromJson和_parseNativeDepFromJson方法
  20.       // 比如cc.Texture2D
  21.       out = {
  22.           preventPreloadNativeObject: ctor.preventPreloadNativeObject,
  23.           preventDeferredLoadDependents: ctor.preventDeferredLoadDependents,
  24.           deps: ctor._parseDepsFromJson(json),
  25.           nativeDep: ctor._parseNativeDepFromJson(json)
  26.       };
  27.       out.nativeDep && (out.nativeDep.uuid = uuid);
  28.   }
  29.   // get deps from an existing asset
  30.   // 如果没有__type__字段,则无法找到它对应的ctor,从asset的__depends__字段中取出依赖
  31.   else {
  32.       if (!CC_EDITOR && (out = this._depends.get(uuid)) && out.parsedFromExistAsset) return out;
  33.       var asset = json;
  34.       out = {
  35.           deps: [],
  36.           parsedFromExistAsset: true,
  37.           preventPreloadNativeObject: asset.constructor.preventPreloadNativeObject,
  38.           preventDeferredLoadDependents: asset.constructor.preventDeferredLoadDependents
  39.       };
  40.       let deps = asset.__depends__;
  41.       for (var i = 0, l = deps.length; i < l; i++) {
  42.           var dep = deps[i].uuid;
  43.           out.deps.push(dep);
  44.       }
  45.   
  46.       if (asset.__nativeDepend__) {
  47.           // asset._nativeDep会返回类似这样的对象 {__isNative__: true, uuid: this._uuid, ext: this._native}
  48.           out.nativeDep = asset._nativeDep;
  49.       }
  50.   }
  51.   // 第一次找到依赖,直接放到_depends列表中,cache dependency list
  52.   this._depends.add(uuid, out);
  53.   return out;
  54. }
复制代码
CCAsset默认的
  1. _parseDepsFromJson
复制代码
  1. _parseNativeDepFromJson
复制代码
实现如下,
  1. _parseDepsFromJson
复制代码
通过调用parseDependRecursively递归json,将json对象及其子对象的所有
  1. __uuid__
复制代码
全部找到放到depends数组中。Texture2D、TTFFont、AudioClip的实现为直接返回空数组,而SpriteFrame的实现为返回
  1. cc.assetManager.utils.decodeUuid(json.content.texture)
复制代码
,这个字段记录了SpriteFrame对应纹理的uuid。
  1. _parseNativeDepFromJson
复制代码
在改asset的
  1. _native
复制代码
有值的环境下,会返回
  1. { __isNative__: true, ext: json._native}
复制代码
。实际上大部分的native资源走的是
  1. _nativeDep
复制代码
,这个属性的get方法会返回一个包罗类似这样的对象
  1. {__isNative__: true, uuid: this._uuid, ext: this._native}
复制代码
  1. _parseDepsFromJson (json) {
  2.       var depends = [];
  3.       parseDependRecursively(json, depends);
  4.       return depends;
  5. },
  6. _parseNativeDepFromJson (json) {
  7. if (json._native) return { __isNative__: true, ext: json._native};
  8.       return null;
  9. }
复制代码
3.5 资源释放

这一末节重点介绍在Creator中释放资源的三种方式以及其背后的实现,末了介绍在项目中怎样排查资源泄露的环境。
3.5.1 Creator的资源释放
Creator支持以下3种资源释放的方式:
            释放方式            释放结果                                    勾选:场景->属性检查器->主动释放资源            在场景切换后,主动释放新场景不利用的资源                            引用计数释放res.decRef            利用addRef和decRef维护引用计数,在decRef后引用计数为0时主动释放                            手动释放cc.assetManager.releaseAsset(texture);            手动释放资源,强制释放        3.5.2 场景主动释放
当一个新场景运行的时候会执行Director.runSceneImmediate方法,这里调用了_autoRelease来实现老场景资源的主动释放(假如老场景勾选了主动释放资源)。
  1. runSceneImmediate: function (scene, onBeforeLoadScene, onLaunched) {
  2.   // 省略代码...
  3.   var oldScene = this._scene;
  4.   if (!CC_EDITOR) {
  5.       // 自动释放资源
  6.       CC_BUILD && CC_DEBUG && console.time('AutoRelease');
  7.       cc.assetManager._releaseManager._autoRelease(oldScene, scene, persistNodeList);
  8.       CC_BUILD && CC_DEBUG && console.timeEnd('AutoRelease');
  9.   }
  10.   // unload scene
  11.   CC_BUILD && CC_DEBUG && console.time('Destroy');
  12.   if (cc.isValid(oldScene)) {
  13.       oldScene.destroy();
  14.   }
  15.   // 省略代码...
  16. },
复制代码
最新版本的_autoRelease的实现非常简洁干脆,将恒久节点的引用从老场景迁移到新场景,然后直接调用资源的decRef淘汰引用计数,而是否释放老场景引用的资源,则取决于老场景是否设置了autoReleaseAssets。
  1. // do auto release
  2. _autoRelease (oldScene, newScene, persistNodes) {
  3.   // 所有持久节点依赖的资源自动addRef、并记录到sceneDeps.persistDeps中
  4.   for (let i = 0, l = persistNodes.length; i < l; i++) {
  5.       var node = persistNodes[i];
  6.       var sceneDeps = dependUtil._depends.get(newScene._id);
  7.       var deps = _persistNodeDeps.get(node.uuid);
  8.       for (let i = 0, l = deps.length; i < l; i++) {
  9.           var dependAsset = assets.get(deps[i]);
  10.           if (dependAsset) {
  11.               dependAsset.addRef();
  12.           }
  13.       }
  14.       if (sceneDeps) {
  15.           !sceneDeps.persistDeps && (sceneDeps.persistDeps = []);
  16.           sceneDeps.persistDeps.push.apply(sceneDeps.persistDeps, deps);
  17.       }
  18.   }
  19.   // 释放老场景的依赖
  20.   if (oldScene) {
  21.       var childs = dependUtil.getDeps(oldScene._id);
  22.       for (let i = 0, l = childs.length; i < l; i++) {
  23.           let asset = assets.get(childs[i]);
  24.           asset && asset.decRef(CC_TEST || oldScene.autoReleaseAssets);
  25.       }
  26.       var dependencies = dependUtil._depends.get(oldScene._id);
  27.       if (dependencies && dependencies.persistDeps) {
  28.           var persistDeps = dependencies.persistDeps;
  29.           for (let i = 0, l = persistDeps.length; i < l; i++) {
  30.               let asset = assets.get(persistDeps[i]);
  31.               asset && asset.decRef(CC_TEST || oldScene.autoReleaseAssets);
  32.           }
  33.       }
  34.       dependUtil.remove(oldScene._id);
  35.   }
  36. },
复制代码
3.5.3 引用计数和手动释放资源
剩下两种释放资源的方式,本质上都是调用releaseManager.tryRelease来实现资源释放,区别在于decRef是根据引用计数和autoRelease来决定是否调用tryRelease,而releaseAsset是强制释放。资源释放的完整流程大致如下图所示:
  1. // CCAsset.js 减少引用
  2. decRef (autoRelease) {
  3.   this._ref--;
  4.   autoRelease !== false && cc.assetManager._releaseManager.tryRelease(this);
  5.   return this;
  6. }
  7. // CCAssetManager.js 手动释放资源
  8. releaseAsset (asset) {
  9.   releaseManager.tryRelease(asset, true);
  10. },
复制代码
tryRelease支持延迟释放和强制释放2种模式,当传入force参数为true时直接进入释放流程,否则creator会将资源放入待释放的列表中,并在
  1. EVENT_AFTER_DRAW
复制代码
变乱中执行freeAssets方法真正清理资源。不论何种方式,资源会传入到_free方法处理,这个方法做了以下几件事故。
       
  • 从_toDelete中移除   
  • 在非force释放时,必要查抄是否还有其它引用,假如是则返回   
  • 从assets缓存中移除   
  • 主动释放依靠资源   
  • 调用资源的destroy方法烧毁资源   
  • 从dependUtil中移除资源的依靠记录
checkCircularReference返回值假如大于0,表现资源还有被其它地方引用,其它地方指所有我们addRef的地方,该方法会先记录asset当前的refCount,然后消除掉资源和依靠资源中对asset的引用,这相当于资源A内部挂载了组件B和C,它们都引用了资源A,此时资源A的引用计数为2,而组件B和C实在是要跟着A释放的,而A被B和C引用着,计数就不为0无法释放,以是checkCircularReference先排除了内部的引用。假如资源的refCount减去了内部的引用次数还大于1,阐明有其它地方还引用着它,不能释放。
  1. tryRelease (asset, force) {
  2.   if (!(asset instanceof cc.Asset)) return;
  3.   if (force) {
  4.       releaseManager._free(asset, force);
  5.   }
  6.   else {
  7.       _toDelete.add(asset._uuid, asset);
  8.       // 在下次Director绘制完成之后,执行freeAssets
  9.       if (!eventListener) {
  10.           eventListener = true;
  11.           cc.director.once(cc.Director.EVENT_AFTER_DRAW, freeAssets);
  12.       }
  13.   }
  14. }
  15. // 释放资源
  16. _free (asset, force) {
  17.   _toDelete.remove(asset._uuid);
  18.   if (!cc.isValid(asset, true)) return;
  19.   if (!force) {
  20.       if (asset.refCount > 0) {
  21.           // 检查资源内部的循环引用
  22.           if (checkCircularReference(asset) > 0) return;
  23.       }
  24.   }
  25.   // 从缓存中移除
  26.   assets.remove(asset._uuid);
  27.   var depends = dependUtil.getDeps(asset._uuid);
  28.   for (let i = 0, l = depends.length; i < l; i++) {
  29.       var dependAsset = assets.get(depends[i]);
  30.       if (dependAsset) {
  31.           dependAsset.decRef(false);
  32.           releaseManager._free(dependAsset, false);
  33.       }
  34.   }
  35.   asset.destroy();
  36.   dependUtil.remove(asset._uuid);
  37. },
  38. // 释放_toDelete中的资源并清空
  39. function freeAssets () {
  40.   eventListener = false;
  41.   _toDelete.forEach(function (asset) {
  42.       releaseManager._free(asset);
  43.   });
  44.   _toDelete.clear();
  45. }
复制代码
asset.destroy做了什么?资源对象是怎样被释放掉的?像纹理、声音这样的资源又是怎样被释放掉的呢?Asset对象自己并没有destroy方法,而是Asset对象所继承的CCObject对象实现了destroy,这里的实现只是将对象放到了一个待释放的数组中,并打上
  1. ToDestroy
复制代码
的标记。Director每一帧都会调用deferredDestroy来执行
  1. _destroyImmediate
复制代码
进行资源释放,这个方法会对对象的Destroyed标记进行判断和操作、调用
  1. _onPreDestroy
复制代码
方法执行回调、以及
  1. _destruct
复制代码
方法进行析构。
  1. prototype.destroy = function () {
  2.     if (this._objFlags & Destroyed) {
  3.         cc.warnID(5000);
  4.         return false;
  5.     }
  6.     if (this._objFlags & ToDestroy) {
  7.         return false;
  8.     }
  9.     this._objFlags |= ToDestroy;
  10.     objectsToDestroy.push(this);
  11.     if (CC_EDITOR && deferredDestroyTimer === null && cc.engine && ! cc.engine._isUpdating) {
  12.         // 在编辑器模式下可以立即销毁
  13.         deferredDestroyTimer = setImmediate(deferredDestroy);
  14.     }
  15.     return true;
  16. };
  17. // Director每一帧都会调用这个方法
  18. function deferredDestroy () {
  19.     var deleteCount = objectsToDestroy.length;
  20.     for (var i = 0; i < deleteCount; ++i) {
  21.         var obj = objectsToDestroy[i];
  22.         if (!(obj._objFlags & Destroyed)) {
  23.             obj._destroyImmediate();
  24.         }
  25.     }
  26.     // 当我们在a.onDestroy中调用b.destroy,objectsToDestroy数组的大小会变化,我们只销毁在这次deferredDestroy之前objectsToDestroy中的元素
  27.     if (deleteCount === objectsToDestroy.length) {
  28.         objectsToDestroy.length = 0;
  29.     }
  30.     else {
  31.         objectsToDestroy.splice(0, deleteCount);
  32.     }
  33.     if (CC_EDITOR) {
  34.         deferredDestroyTimer = null;
  35.     }
  36. }
  37. // 真正的资源释放
  38. prototype._destroyImmediate = function () {
  39.     if (this._objFlags & Destroyed) {
  40.         cc.errorID(5000);
  41.         return;
  42.     }
  43.     // 执行回调
  44.     if (this._onPreDestroy) {
  45.         this._onPreDestroy();
  46.     }
  47.     if ((CC_TEST ? (/* make CC_EDITOR mockable*/ Function('return !CC_EDITOR'))() : !CC_EDITOR) || cc.engine._isPlaying) {
  48.         this._destruct();
  49.     }
  50.     this._objFlags |= Destroyed;
  51. };
复制代码
在这里
  1. _destruct
复制代码
做的事故就是将对象的属性清空,比如将object范例的属性置为null,将string范例的属性置为'',compileDestruct方法会返回一个该类的析构函数,compileDestruct先网络了平凡object和cc.Class这两种范例下的所有属性,并根据范例构建了一个propsToReset用来清空属性,支持JIT的环境下会根据要清空的属性天生一个类似这样的函数返回
  1. function(o) {o.a='';o.b=null;o.['c']=undefined...}
复制代码
,而非JIT环境下会返回一个根据propsToReset遍历处理的函数,前者占用更多内存,但效率更高。
  1. prototype._destruct = function () {
  2.     var ctor = this.constructor;
  3.     var destruct = ctor.__destruct__;
  4.     if (!destruct) {
  5.         destruct = compileDestruct(this, ctor);
  6.         js.value(ctor, '__destruct__', destruct, true);
  7.     }
  8.     destruct(this);
  9. };
  10. function compileDestruct (obj, ctor) {
  11.     var shouldSkipId = obj instanceof cc._BaseNode || obj instanceof cc.Component;
  12.     var idToSkip = shouldSkipId ? '_id' : null;
  13.     var key, propsToReset = {};
  14.     for (key in obj) {
  15.         if (obj.hasOwnProperty(key)) {
  16.             if (key === idToSkip) {
  17.                 continue;
  18.             }
  19.             switch (typeof obj[key]) {
  20.                 case 'string':
  21.                     propsToReset[key] = '';
  22.                     break;
  23.                 case 'object':
  24.                 case 'function':
  25.                     propsToReset[key] = null;
  26.                     break;
  27.             }
  28.         }
  29.     }
  30.     // Overwrite propsToReset according to Class
  31.     if (cc.Class._isCCClass(ctor)) {
  32.         var attrs = cc.Class.Attr.getClassAttrs(ctor);
  33.         var propList = ctor.__props__;
  34.         for (var i = 0; i < propList.length; i++) {
  35.             key = propList[i];
  36.             var attrKey = key + cc.Class.Attr.DELIMETER + 'default';
  37.             if (attrKey in attrs) {
  38.                 if (shouldSkipId && key === '_id') {
  39.                     continue;
  40.                 }
  41.                 switch (typeof attrs[attrKey]) {
  42.                     case 'string':
  43.                         propsToReset[key] = '';
  44.                         break;
  45.                     case 'object':
  46.                     case 'function':
  47.                         propsToReset[key] = null;
  48.                         break;
  49.                     case 'undefined':
  50.                         propsToReset[key] = undefined;
  51.                         break;
  52.                 }
  53.             }
  54.         }
  55.     }
  56.     if (CC_SUPPORT_JIT) {
  57.         // compile code
  58.         var func = '';
  59.         for (key in propsToReset) {
  60.             var statement;
  61.             if (CCClass.IDENTIFIER_RE.test(key)) {
  62.                 statement = 'o.' + key + '=';
  63.             }
  64.             else {
  65.                 statement = 'o[' + CCClass.escapeForJS(key) + ']=';
  66.             }
  67.             var val = propsToReset[key];
  68.             if (val === '') {
  69.                 val = '""';
  70.             }
  71.             func += (statement + val + ';\n');
  72.         }
  73.         return Function('o', func);
  74.     }
  75.     else {
  76.         return function (o) {
  77.             for (var key in propsToReset) {
  78.                 o[key] = propsToReset[key];
  79.             }
  80.         };
  81.     }
  82. }
复制代码
那么
  1. _onPreDestroy
复制代码
又做了什么呢?主要是将各种变乱、定时器进行注销,对子节点、组件等进行删除,详情可以看下面这段代码。
  1. // Node的_onPreDestroy
  2. _onPreDestroy () {
  3.   // 调用_onPreDestroyBase方法,实际是调用BaseNode.prototype._onPreDestroy,这个方法下面介绍
  4.   var destroyByParent = this._onPreDestroyBase();
  5.   // 注销Actions
  6.   if (ActionManagerExist) {
  7.       cc.director.getActionManager().removeAllActionsFromTarget(this);
  8.   }
  9.   // 移除_currentHovered
  10.   if (_currentHovered === this) {
  11.       _currentHovered = null;
  12.   }
  13.   this._bubblingListeners && this._bubblingListeners.clear();
  14.   this._capturingListeners && this._capturingListeners.clear();
  15.   // 移除所有触摸和鼠标事件监听
  16.   if (this._touchListener || this._mouseListener) {
  17.       eventManager.removeListeners(this);
  18.       if (this._touchListener) {
  19.           this._touchListener.owner = null;
  20.           this._touchListener.mask = null;
  21.           this._touchListener = null;
  22.       }
  23.       if (this._mouseListener) {
  24.           this._mouseListener.owner = null;
  25.           this._mouseListener.mask = null;
  26.           this._mouseListener = null;
  27.       }
  28.   }
  29.   if (CC_JSB && CC_NATIVERENDERER) {
  30.       this._proxy.destroy();
  31.       this._proxy = null;
  32.   }
  33.   // 回收到对象池中
  34.   this._backDataIntoPool();
  35.   if (this._reorderChildDirty) {
  36.       cc.director.__fastOff(cc.Director.EVENT_AFTER_UPDATE, this.sortAllChildren, this);
  37.   }
  38.   if (!destroyByParent) {
  39.       if (CC_EDITOR) {
  40.           // 确保编辑模式下的,节点的被删除后可以通过ctrl+z撤销(重新添加到原来的父节点)
  41.           this._parent = null;
  42.       }
  43.   }
  44. },
  45. // BaseNode的_onPreDestroy
  46. _onPreDestroy () {
  47.   var i, len;
  48.   // 加上Destroying标记
  49.   this._objFlags |= Destroying;
  50.   var parent = this._parent;
  51.   
  52.   // 根据检测父节点的标记判断是不是由父节点的destroy发起的释放
  53.   var destroyByParent = parent && (parent._objFlags & Destroying);
  54.   if (!destroyByParent && (CC_EDITOR || CC_TEST)) {
  55.       // 从编辑器中移除
  56.       this._registerIfAttached(false);
  57.   }
  58.   // 把所有子节点进行释放,它们的_onPreDestroy也会被执行
  59.   var children = this._children;
  60.   for (i = 0, len = children.length; i < len; ++i) {
  61.       children[i]._destroyImmediate();
  62.   }
  63.   // 把所有的组件进行释放,它们的_onPreDestroy也会被执行
  64.   for (i = 0, len = this._components.length; i < len; ++i) {
  65.       var component = this._components[i];
  66.       component._destroyImmediate();
  67.   }
  68.   // 注销事件监听,比如otherNode.on(type, callback, thisNode) 注册了事件
  69.   // thisNode被释放时,需要注销otherNode身上的监听,避免事件回调到已销毁的对象上
  70.   var eventTargets = this.__eventTargets;
  71.   for (i = 0, len = eventTargets.length; i < len; ++i) {
  72.       var target = eventTargets[i];
  73.       target && target.targetOff(this);
  74.   }
  75.   eventTargets.length = 0;
  76.   // 如果自己是常驻节点,则从常驻节点列表中移除
  77.   if (this._persistNode) {
  78.       cc.game.removePersistRootNode(this);
  79.   }
  80.   // 如果是自己释放的自己,而不是从父节点释放的,要通知父节点,把这个失效的子节点移除掉
  81.   if (!destroyByParent) {
  82.       if (parent) {
  83.           var childIndex = parent._children.indexOf(this);
  84.           parent._children.splice(childIndex, 1);
  85.           parent.emit && parent.emit('child-removed', this);
  86.       }
  87.   }
  88.   return destroyByParent;
  89. },
  90. // Component的_onPreDestroy
  91. _onPreDestroy () {
  92.   // 移除ActionManagerExist和schedule
  93.   if (ActionManagerExist) {
  94.       cc.director.getActionManager().removeAllActionsFromTarget(this);
  95.   }
  96.   this.unscheduleAllCallbacks();
  97.   // 移除所有的监听
  98.   var eventTargets = this.__eventTargets;
  99.   for (var i = eventTargets.length - 1; i >= 0; --i) {
  100.       var target = eventTargets[i];
  101.       target && target.targetOff(this);
  102.   }
  103.   eventTargets.length = 0;
  104.   // 编辑器模式下停止监控
  105.   if (CC_EDITOR && !CC_TEST) {
  106.       _Scene.AssetsWatcher.stop(this);
  107.   }
  108.   // destroyComp的实现为调用组件的onDestroy回调,各个组件会在回调中销毁自身的资源
  109.   // 比如RigidBody3D组件会调用body的destroy方法,而Animation组件会调用stop方法
  110.   cc.director._nodeActivator.destroyComp(this);
  111.   // 将组件从节点身上移除
  112.   this.node._removeComponent(this);
  113. },   
复制代码
3.5.4 资源释放的题目
末了我们来聊一聊资源释放的题目与定位,在加入引用计数后,最常见的题目还是没有精确增减引用计数导致的内存泄露(循环引用、少调用了decRef或多调用了addRef),以及正在利用的资源被释放的题目(和内存泄露相反,资源被提前释放了)。
从目前的代码来看,假如精确利用了引用计数,新的资源底层是可以克制内存泄露等题目的
这种题目怎么办理呢?首先是定位出哪些资源出了题目,假如是被提前释放,我们可以直接定位到这个资源,假如是内存泄露,当我们发现题目时程序通常已经占用了大量的内存,这种环境下可以切换到一个空场景,并清理资源,把资源清理完后,可以查抄assets中残留的资源是否有未被释放的资源。
要相识资源为什么会泄露,可以通过跟踪addRef和decRef的调用得到,下面提供了一个示例方法,用于跟踪某资源的addRef和decRef调用,然后调用资源的dump方法打印出所有调用的堆栈:
  1. public static traceObject(obj : cc.Asset) {
  2.   let addRefFunc = obj.addRef;
  3.   let decRefFunc = obj.decRef;
  4.   let traceMap = new Map();
  5.   obj.addRef = function() : cc.Asset {
  6.       let stack = ResUtil.getCallStack(1);
  7.       let cnt = traceMap.has(stack) ? traceMap.get(stack) + 1 : 1;
  8.       traceMap.set(stack, cnt);
  9.       return addRefFunc.apply(obj, arguments);
  10.   }
  11.   obj.decRef = function() : cc.Asset {
  12.       let stack = ResUtil.getCallStack(1);
  13.       let cnt = traceMap.has(stack) ? traceMap.get(stack) + 1 : 1;
  14.       traceMap.set(stack, cnt);
  15.       return decRefFunc.apply(obj, arguments);
  16.   }
  17.   obj['dump'] = function() {
  18.       console.log(traceMap);
  19.   }
  20. }
复制代码
以上就是剖析CocosCreator新资源管理系统的详细内容,更多关于CococCreator的资料,请关注脚本之家其他相干文章!

本帖子中包含更多资源

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

x

帖子地址: 

回复

使用道具 举报

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

本版积分规则

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

  • 微信公众号

  • 商务合作