当前位置 博文首页 > Cocos Creator 新资源管理系统剖析

    Cocos Creator 新资源管理系统剖析

    作者:深圳-宝爷 时间:2021-01-11 06:02

    目录
    • 1.资源与构建
      • 1.1 creator资源文件基础
      • 1.2 资源构建
        • 1.2.1 图片、图集、自动图集
        • 1.2.2 Prefab与场景
        • 1.2.3 资源文件合并规则
    • 2. 理解与使用 Asset Bundle
      • 2.1 创建Bundle
      • 2.2 使用Bundle
    • 3. 新资源框架剖析
      • 3.1 加载管线
        • 3.1.1 启动加载管线【加载接口】
        • 3.1.2 transformPipeline管线【准备阶段】
        • 3.1.3 load管线【加载流程】
      • 3.2 文件下载
        • 3.2.1 Web平台的下载
        • 3.2.2 原生平台下载
      • 3.3 文件解析
        • 3.3.1 Web平台解析
        • 3.3.2 原生平台解析
      • 3.4 依赖加载
        • 3.4.1 依赖解析
      • 3.5 资源释放
        • 3.5.1 Creator的资源释放
        • 3.5.2 场景自动释放
        • 3.5.3 引用计数和手动释放资源
        • 3.5.4 资源释放的问题

    v2.4开始,Creator使用AssetBundle完全重构了资源底层,提供了更加灵活强大的资源管理方式,也解决了之前版本资源管理的痛点(资源依赖与引用),本文将带你深入了解Creator的新资源底层。

    • 资源与构建
    • 理解与使用AssetBundle
    • 新资源框架剖析
      • 加载管线
      • 文件下载
      • 文件解析
      • 依赖加载
      • 资源释放

    1.资源与构建

    1.1 creator资源文件基础

    在了解引擎如何解析、加载资源之前,我们先来了解一下这些资源文件(图片、Prefab、动画等)的规则,在creator项目目录下有几个与资源相关的目录:

    • assets 所有资源的总目录,对应creator编辑器的资源管理器
    • library 本地资源库,预览项目时使用的目录
    • build 构建后的项目默认目录

    在assets目录下,creator会为每个资源文件和目录生成一个同名的.meta文件,meta文件是一个json文件,记录了资源的版本、uuid以及各种自定义的信息(在编辑器的属性检查器中设置),比如prefab的meta文件,就记录了我们可以在编辑器修改的optimizationPolicy和asyncLoadAssets等属性。

    {
      "ver": "1.2.7",
      "uuid": "a8accd2e-6622-4c31-8a1e-4db5f2b568b5",
      "optimizationPolicy": "AUTO",     // prefab创建优化策略
      "asyncLoadAssets": false,         // 是否延迟加载
      "readonly": false,
      "subMetas": {}
    }
    

    在library目录下的imports目录,资源文件名会被转换成uuid,并取uuid前2个字符进行目录分组存放,creator会将所有资源的uuid到assets目录的映射关系,以及资源和meta的最后更新时间戳放到一个名为uuid-to-mtime.json的文件中,如下所示。

    {
      "9836134e-b892-4283-b6b2-78b5acf3ed45": {
        "asset": 1594351233259,
        "meta": 1594351616611,
        "relativePath": "effects"
      },
      "430eccbf-bf2c-4e6e-8c0c-884bbb487f32": {
        "asset": 1594351233254,
        "meta": 1594351616643,
        "relativePath": "effects\\__builtin-editor-gizmo-line.effect"
      },
      ...
    }
    

    与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文件会被压缩成一个,如果选择无压缩,则每个图片都会生成一个Texture2D的json文件。

    {
      "__type__": "cc.Texture2D",
      "content": "0,9729,9729,33071,33071,0,0,1"
    }
    

    如果将纹理的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文件,在勾选内联所有SpriteFrame的情况下,Prefab引用到的SpriteFrame会被合并到prefab所在的json文件中,如果一个SpriteFrame被多个prefab引用,那么每个prefab的json文件都会包含该SpriteFrame的信息。而在没有勾选内联所有SpriteFrame的情况下,SpriteFrame会是单独的json文件。

    1.2.3 资源文件合并规则

    当Creator将多个资源合并到一个json文件中,我们可以在config.json中的packs字段找到被打包的资源信息,一个资源有可能被重复打包到多个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平台,建议勾选内联所有SpriteFrame这可以减少网络io,提高性能,而原生平台不建议勾选,这可能会增加包体大小以及热更时要下载的内容。对于一些紧凑的Bundle(比如加载该Bundle就需要用到里面所有的资源),我们可以配置为合并所有的json。

    2. 理解与使用 Asset Bundle

    2.1 创建Bundle

    Asset Bundle是creator 2.4之后的资源管理方案,简单地说就是通过目录来对资源进行规划,按照项目的需求将各种资源放到不同的目录下,并将目录配置成Asset Bundle。能够起到以下作用:

    • 加快游戏启动时间
    • 减小首包体积
    • 跨项目复用资源
    • 方便实现子游戏
    • 以Bundle为单位的热更新

    Asset Bundle的创建非常简单,只要在目录的属性检查器中勾选配置为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来加载

    cc.resources.load("test assets/prefab", function (err, prefab) {
        var newNode = cc.instantiate(prefab);
        cc.director.getScene().addChild(newNode);
    });
    

    如果是其它自定义Bundle(本地Bundle或远程Bundle都可以用Bundle名加载),可以使用cc.assetManager.loadBundle来加载Bundle,然后使用加载后的Bundle对象,来加载Bundle中的资源。对于原生平台,如果Bundle被配置为远程包,在构建时需要在构建发布面板中填写资源服务器地址。

    cc.assetManager.loadBundle('01_graphics', (err, bundle) => {
        bundle.load('xxx');
    });
    

    原生或小游戏平台下,我们还可以这样使用Bundle:

    • 如果要加载其它项目的远程Bundle,则需要使用url的方式加载(其它项目指另一个cocos工程)
    • 如果希望自己管理Bundle的下载和缓存,可以放到本地可写路径,并传入路径来加载这些Bundle
    // 当复用其他项目的 Asset Bundle 时
    cc.assetManager.loadBundle('https://othergame.com/remote/01_graphics', (err, bundle) => {
        bundle.load('xxx');
    });
    
    // 原生平台
    cc.assetManager.loadBundle(jsb.fileUtils.getWritablePath() + '/pathToBundle/bundleName', (err, bundle) => {
        // ...
    });
    
    // 微信小游戏平台
    cc.assetManager.loadBundle(wx.env.USER_DATA_PATH + '/pathToBundle/bundleName', (err, bundle) => {
        // ...
    });
    

    其它注意项:

    • 加载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对象,并设置其__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资源的封装,可以通过 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条管线,普通的加载管线、预加载、以及资源路径转换管线,最后这条管线是为前面两条管线服务的。

        // 正常加载
        this.pipeline = pipeline.append(preprocess).append(load);
        // 预加载
        this.fetchPipeline = fetchPipeline.append(preprocess).append(fetch);
        // 转换资源路径
        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__等字段

        // bundle类的load方法
        load (paths, type, onProgress, onComplete) {
            var { type, onProgress, onComplete } = parseLoadResArgs(type, onProgress, onComplete);
            cc.assetManager.loadAny(paths, { __requestType__: RequestType.PATH, type: type, bundle: this.name }, onProgress, onComplete);
        },
        
        // assetManager的loadAny方法
        loadAny (requests, options, onProgress, onComplete) {
            var { options, onProgress, onComplete } = parseParameters(options, onProgress, onComplete);
            
            options.preset = options.preset || 'default';
            let task = new Task({input: requests, onProgress, onComplete: asyncify(onComplete), options});
            pipeline.async(task);
        },
    

    pipeline由两部分组成 preprocess 和 load。preprocess 由以下管线组成 preprocess、transformPipeline { parse、combine },preprocess实际上只创建了一个子任务,然后交由transformPipeline执行。对于加载一个普通的资源,子任务的input和options与父任务相同。

        let subTask = Task.create({input: task.input, options: subOptions});
        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类型是远程资源类型,无需特殊处理
    function parse (task) {
        // 将input转换成数组
        var input = task.input, options = task.options;
        input = Array.isArray(input) ? input : [ input ];
    
        task.output = [];
        for (var i = 0; i < input.length; i ++ ) {
            var item = input[i];
            var out = RequestItem.create();
            if (typeof item === 'string') {
                // 先创建object
                item = Object.create(null);
                item[options.__requestType__ || RequestType.UUID] = input[i];
            }
            if (typeof item === 'object') {
                // local options will overlap glabal options
                // 将options的属性复制到item身上,addon会复制options上有,而item没有的属性
                cc.js.addon(item, options);
                if (item.preset) {
                    cc.js.addon(item, cc.assetManager.presets[item.preset]);
                }
                for (var key in item) {
                    switch (key) {
                        // uuid类型资源,从bundle中取出该资源的详细信息
                        case RequestType.UUID: 
                            var uuid = out.uuid = decodeUuid(item.uuid);
                            if (bundles.has(item.bundle)) {
                                var config = bundles.get(item.bundle)._config;
                                var info = config.getAssetInfo(uuid);
                                if (info && info.redirect) {
                                    if (!bundles.has(info.redirect)) throw new Error(`Please load bundle ${info.redirect} first`);
                                    config = bundles.get(info.redirect)._config;
                                    info = config.getAssetInfo(uuid);
                                }
                                out.config = config;
                                out.info = info;
                            }
                            out.ext = item.ext || '.json';
                            break;
                        case '__requestType__':
                        case 'ext': 
                        case 'bundle':
                        case 'preset':
                        case 'type': break;
                        case RequestType.DIR: 
                            // 解包后动态添加到input列表尾部,后续的循环会自动parse这些资源
                            if (bundles.has(item.bundle)) {
                                var infos = [];
                                bundles.get(item.bundle)._config.getDirWithPath(item.dir, item.type, infos);
                                for (let i = 0, l = infos.length; i < l; i++) {
                                    var info = infos[i];
                                    input.push({uuid: info.uuid, __isNative__: false, ext: '.json', bundle: item.bundle});
                                }
                            }
                            out.recycle();
                            out = null;
                            break;
                        case RequestType.PATH: 
                            // PATH类型的资源根据路径和type取出该资源的详细信息
                            if (bundles.has(item.bundle)) {
                                var config = bundles.get(item.bundle)._config;
                                var info = config.getInfoWithPath(item.path, item.type);
                                
                                if (info && info.redirect) {
                                    if (!bundles.has(info.redirect)) throw new Error(`you need to load bundle ${info.redirect} first`);
                                    config = bundles.get(info.redirect)._config;
                                    info = config.getAssetInfo(info.uuid);
                                }
    
                                if (!info) {
                                    out.recycle();
                                    throw new Error(`Bundle ${item.bundle} doesn't contain ${item.path}`);
                                }
                                out.config = config; 
                                out.uuid = info.uuid;
                                out.info = info;
                            }
                            out.ext = item.ext || '.json';
                            break;
                        case RequestType.SCENE:
                            // 场景类型,从bundle中的config调用getSceneInfo取出该场景的详细信息
                            if (bundles.has(item.bundle)) {
                                var config = bundles.get(item.bundle)._config;
                                var info = config.getSceneInfo(item.scene);
                                
                                if (info && info.redirect) {
                                    if (!bundles.has(info.redirect)) throw new Error(`you need to load bundle ${info.redirect} first`);
                                    config = bundles.get(info.redirect)._config;
                                    info = config.getAssetInfo(info.uuid);
                                }
                                if (!info) {
                                    out.recycle();
                                    throw new Error(`Bundle ${config.name} doesn't contain scene ${item.scene}`);
                                }
                                out.config = config; 
                                out.uuid = info.uuid;
                                out.info = info;
                            }
                            break;
                        case '__isNative__': 
                            out.isNative = item.__isNative__;
                            break;
                        case RequestType.URL: 
                            out.url = item.url;
                            out.uuid = item.uuid || item.url;
                            out.ext = item.ext || cc.path.extname(item.url);
                            out.isNative = item.__isNative__ !== undefined ? item.__isNative__ : true;
                            break;
                        default: out.options[key] = item[key];
                    }
                    if (!out) break;
                }
            }
            if (!out) continue;
            task.output.push(out);
            if (!out.uuid && !out.url) throw new Error('unknown input:' + item.toString());
        }
        return null;
    }
    

    RequestItem的初始信息,都是从bundle对象中查询的,bundle的信息则是从bundle自带的config.json文件中初始化的,在打包bundle的时候,会将bundle中的资源信息写入config.json中。

    经过parse方法处理后,我们会得到一系列RequestItem,并且很多RequestItem都自带了AssetInfo和uuid等信息,combine方法会为每个RequestItem构建出真正的加载路径,这个加载路径最终会转换到item.url中。

    function combine (task) {
        var input = task.output = task.input;
        for (var i = 0; i < input.length; i++) {
            var item = input[i];
            // 如果item已经包含了url,则跳过,直接使用item的url
            if (item.url) continue;
    
            var url = '', base = '';
            var config = item.config;
            // 决定目录的前缀
            if (item.isNative) {
                base = (config && config.nativeBase) ? (config.base + config.nativeBase) : cc.assetManager.generalNativeBase;
            } 
            else {
                base = (config && config.importBase) ? (config.base + config.importBase) : cc.assetManager.generalImportBase;
            }
    
            let uuid = item.uuid;
                
            var ver = '';
            if (item.info) {
                if (item.isNative) {
                    ver = item.info.nativeVer ? ('.' + item.info.nativeVer) : '';
                }
                else {
                    ver = item.info.ver ? ('.' + item.info.ver) : '';
                }
            }
    
            // 拼接最终的url
            // ugly hack, WeChat does not support loading font likes 'myfont.dw213.ttf'. So append hash to directory
            if (item.ext === '.ttf') {
                url = `${base}/${uuid.slice(0, 2)}/${uuid}${ver}/${item.options.__nativeName__}`;
            }
            else {
                url = `${base}/${uuid.slice(0, 2)}/${uuid}${ver}${item.ext}`;
            }
            
            item.url = url;
        }
        return null;
    }
    

    3.1.3 load管线【加载流程】

    load方法做的事情很简单,基本只是创建了新的任务,在loadOneAssetPipeline中执行每个子任务

    function load (task, done) {
        if (!task.progress) {
            task.progress = {finish: 0, total: task.input.length};
        }
        
        var options = task.options, progress = task.progress;
        options.__exclude__ = options.__exclude__ || Object.create(null);
        task.output = [];
        forEach(task.input, function (item, cb) {
            // 对每个input项都创建一个子任务,并交由loadOneAssetPipeline执行
            let subTask = Task.create({ 
                input: item, 
                onProgress: task.onProgress, 
                options, 
                progress, 
                onComplete: function (err, item) {
                    if (err && !task.isFinish && !cc.assetManager.force) done(err);
                    task.output.push(item);
                    subTask.recycle();
                    cb();
                }
            });
            // 执行子任务,loadOneAssetPipeline有fetch和parse组成
            loadOneAssetPipeline.async(subTask);
        }, function () {
            // 每个input执行完成后,最后执行该函数
            options.__exclude__ = null;
            if (task.isFinish) {
                clear(task, true);
                return task.dispatch('error');
            }
            gatherAsset(task);
            clear(task, true);
            done();
        });
    }
    

    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在task.options.__exclude__中,则标记为完成,并添加引用计数
        • 否则,根据一些复杂的条件来决定是否加载资源的依赖
    var loadOneAssetPipeline = new Pipeline('loadOneAsset', [
        function fetch (task, done) {
            var item = task.output = task.input;
            var { options, isNative, uuid, file } = item;
            var { reload } = options;
            // 如果assets里面已经加载了这个资源,则直接完成
            if (file || (!reload && !isNative && assets.has(uuid))) return done();
            // 下载文件,这是一个异步的过程,文件下载完会被放到item.file中,并执行done驱动管线
            packManager.load(item, task.options, function (err, data) {
                if (err) {
                    if (cc.assetManager.force) {
                        err = null;
                    } else {
                        cc.error(err.message, err.stack);
                    }
                    data = null;
                }
                item.file = data;
                done(err);
            });
        },
        // 将资源文件转换成资源对象的过程
        function parse (task, done) {
            var item = task.output = task.input, progress = task.progress, exclude = task.options.__exclude__;
            var { id, file, options } = item;
    
            if (item.isNative) {
                // 对于原生资源,调用parser.parse进行处理,将处理完的资源放到item.content中,并结束流程
                parser.parse(id, file, item.ext, options, function (err, asset) {
                    if (err) {
                        if (!cc.assetManager.force) {
                            cc.error(err.message, err.stack);
                            return done(err);
                        }
                    }
                    item.content = asset;
                    task.dispatch('progress', ++progress.finish, progress.total, item);
                    files.remove(id);
                    parsed.remove(id);
                    done();
                });
            } else {
                var { uuid } = item;
                // 非原生资源,如果在task.options.__exclude__中,直接结束
                if (uuid in exclude) {
                    var { finish, content, err, callbacks } = exclude[uuid];
                    task.dispatch('progress', ++progress.finish, progress.total, item);
        
                    if (finish || checkCircleReference(uuid, uuid, exclude) ) {
                        content && content.addRef();
                        item.content = content;
                        done(err);
                    } else {
                        callbacks.push({ done, item });
                    }
                } else {
                    // 如果不是reload,且asset中包含了该uuid
                    if (!options.reload && assets.has(uuid)) {
                        var asset = assets.get(uuid);
                        // 开启了options.__asyncLoadAssets__,或asset.__asyncLoadAssets__为false,直接结束,不加载依赖
                        if (options.__asyncLoadAssets__ || !asset.__asyncLoadAssets__) {
                            item.content = asset.addRef();
                            task.dispatch('progress', ++progress.finish, progress.total, item);
                            done();
                        }
                        else {
                            loadDepends(task, asset, done, false);
                        }
                    } else {
                        // 如果是reload,或者assets中没有,则进行解析,并加载依赖
                        parser.parse(id, file, 'import', options, function (err, asset) {
                            if (err) {
                                if (cc.assetManager.force) {
                                    err = null;
                                }
                                else {
                                    cc.error(err.message, err.stack);
                                }
                                return done(err);
                            }
                            
                            asset._uuid = uuid;
                            loadDepends(task, asset, done, true);
                        });
                    }
                }
            }
        }
    ]);
    

    3.2 文件下载

    creator使用packManager.load来完成下载的工作,当要下载一个文件时,有2个问题需要考虑:

    • 该文件是否被打包了,比如由于勾选了内联所有SpriteFrame,导致SpriteFrame的json文件被合并到prefab中
    • 当前平台是原生平台还是web平台,对于一些本地资源,原生平台需要从磁盘读取
        // packManager.load的实现
        load (item, options, onComplete) {
            // 如果资源没有被打包,则直接调用downloader.download下载(download内部也有已下载和加载中的判断)
            if (item.isNative || !item.info || !item.info.packs) return downloader.download(item.id, item.url, item.ext, item.options, onComplete);
            // 如果文件已经下载过了,则直接返回
            if (files.has(item.id)) return onComplete(null, files.get(item.id));
    
            var packs = item.info.packs;
            // 如果pack已经在加载中,则将回调添加到_loading队列,等加载完成后触发回调
            var pack = packs.find(isLoading);
            if (pack) return _loading.get(pack.uuid).push({ onComplete, id: item.id });
    
            // 下载一个新的pack
            pack = packs[0];
            _loading.add(pack.uuid, [{ onComplete, id: item.id }]);
            let url = cc.assetManager._transform(pack.uuid, {ext: pack.ext, bundle: item.config.name});
            // 下载pack并解包,
            downloader.download(pack.uuid, url, pack.ext, item.options, function (err, data) {
                files.remove(pack.uuid);
                if (err) {
                    cc.error(err.message, err.stack);
                }
                // unpack package,内部实现包含2种解包,一种针对prefab、图集等json数组的分割解包,另一种针对Texture2D的content进行解包
                packManager.unpack(pack.packs, data, pack.ext, item.options, function (err, result) {
                    if (!err) {
                        for (var id in result) {
                            files.add(id, result[id]);
                        }
                    }
                    var callbacks = _loading.remove(pack.uuid);
                    for (var i = 0, l = callbacks.length; i < l; i++) {
                        var cb = callbacks[i];
                        if (err) {
                            cb.onComplete(err);
                            continue;
                        }
    
                        var data = result[cb.id];
                        if (!data) {
                            cb.onComplete(new Error('can not retrieve data from package'));
                        }
                        else {
                            cb.onComplete(null, data);
                        }
                    }
                });
            });
        }
    
    
    下一篇:没有了