[CocosCreator]AssetManager之管线

Posted ouyangshima

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[CocosCreator]AssetManager之管线相关的知识,希望对你有一定的参考价值。

creator 使用管线(pipeline)来处理整个资源加载的流程,这样的好处是解耦了资源处理的流程,将每一个步骤独立成一个单独的管道,管道可以很方便地进行复用和组合,并且方便了我们自定义整个加载流程,我们可以创建一些自己的管道,加入到管线中,比如资源加密。

管线

管线 可以理解为一系列过程的串联组合,当一个请求经过管线时,会被管线的各个阶段依次进行处理,最后输出处理后的结果。如下图所示:

管线与一般的固定流程相比,优势在于管线中的所有环节都是可拼接和组合的,这意味着开发者可以在现有管线的任意环节插入新的阶段或者移除旧的阶段,极大地增强了灵活性和可扩展性

任务

任务 就是在管线中流动的请求,一个任务中包括输入、输出、完成回调、可选参数 等内容。当任务在管线中流动时,管线的各个阶段会取出任务的输入,做出一定的处理后存回到输出中。

UML

  • 管线pipeline是存储管道的数组容器
  • 管道pipe是fun(task,done)形式参数的处理函数,pipeline同步执行时只需要task参数,不需要done参数。
  • 任务task是管道处理函数的参数

内置管线

AssetManager 内置了3条管线,普通的加载管线、预加载、以及资源路径转换管线,最后这条管线是为前面两条管线服务的。

    // 正常加载
    this.pipeline = pipeline.append(preprocess).append(load);
    // 预加载
    this.fetchPipeline = fetchPipeline.append(preprocess).append(fetch);
    // 转换资源路径
    this.transformPipeline = transformPipeline.append(parse).append(combine);

  • 第一条管线用于转换资源路径,找到真实资源路径。为正常加载管线和预加载管线服务。
  • 第二条管线用于正常加载,用到了下载器和解析器
  • 第三条管线用于预加载,用到了下载器。

pipeline正常加载管线

pipeline 由两部分组成 preprocessload

启动加载管线【加载接口】

一个普通的资源是如何加载的,比如最简单的 cc.resource.load,在 bundle.load 方法中,调用了 cc.assetManager.loadAny,在 loadAny 方法中,创建了一个新的任务,并调用正常加载管线 pipelineasync 方法执行任务。

    // 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);
    },

preprocess预处理管道【准备阶段】

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

    let subTask = Task.create({input: task.input, options: subOptions});
    task.output = task.source = transformPipeline.sync(subTask);

transformPipeline 路径转换管线

transformPipelineparsecombine 两个管线组成,parse 的职责是为每个要加载的资源生成 RequestItem对象并初始化其资源信息(AssetInfo、uuid、config等):

先将 input转换成数组进行遍历,如果是批量加载资源,每个加载项都会生成RequestItem

如果输入的 itemobject,则先将 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 都自带了 AssetInfouuid 等信息,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;
}

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步,fetchparse

  1. fetch方法:用于下载资源文件,由 packManager 负责下载的实现,fetch 会将下载完的文件数据放到 item.file 中。
  2. parse方法:用于将加载完的资源文件转换成我们可用的资源对象, parser.parse 会将解析后的资源对象放到 item.content 中。
  • 对于原生资源,调用 parser.parse 进行解析,该方法会根据资源类型调用不同的解析方法
  • import 资源调用 parseImport 方法,根据 json 数据反序列化出 Asset 对象,并放到 assets
  • 图片资源会调用 parseImageparsePVRTexparsePKMTex方法解析图像格式(但不会创建Texture对象)
  • 音效资源调用 parseAudio 方法进行解析
  • plist 资源调用 parsePlist 方法进行解析
  • 对于其它资源,如果 uuidtask.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);
                    });
                }
            }
        }
    }
]);

以上是关于[CocosCreator]AssetManager之管线的主要内容,如果未能解决你的问题,请参考以下文章

CocosCreator入门CocosCreator组件 | LabelOutline(文本描边)组件 | LabelShadow(文本阴影) 组件

cocoscreator怎么访问button的label

cocoscreator内存占用多少算高

cocoscreator卡在加载资源

CocosCreator入门 ------ 简介安装

cocoscreator模拟器怎么触摸