Cocos Creator2.4.8 资源加载源码阅读

最近用到资源加载的问题:加载的资源进度条倒退问题,现在只是用临时的解决方案 - 加载进度如果出现会倒退的问题还是用原来的数值。抽时间看了下cocos creator 资源加载的源码,整理了一下脑图

一: 关于资源加载管线的几个疑问:

1:任务是什么时候开始创建的,怎么流通的。

2:assetManager.assets什么时候装入的。

3:三条管线都做了哪些操作。

4:资源的加载进度为什么会倒退。

二: 看源码分析下上面几个问题,首先说明我没有具体看fetchPipeline,这个只是预加载的管线。

1: 任务是什么时候开始创建的,怎么流通的

a: 创建:第一个任务是从window.boot里面来的:

  

var count = 0;
        function cb (err) {
            if (err) return console.error(err);
            count++;
            if (count === bundleRoot.length + 1) {
                cc.assetManager.loadBundle(MAIN, function (err) {
                    if (!err) cc.game.run(option, onStart);
                });
            }
        }

        // load plugins 加载插件脚本
        cc.assetManager.loadScript(_CCSettings.jsList.map(function (x) { return '/plugins/' + x; }), cb);

        // load bundles 加载bundle
        for (var i = 0; i < bundleRoot.length; i++) {
            cc.assetManager.loadBundle(bundleRoot[i], cb);
        }

这里loadScript调用的是loadAny:

// CCAssetManager.js
loadScript (url, options, onComplete) {
        var { options, onComplete } = parseParameters(options, undefined, onComplete);
        options.__requestType__ = RequestType.URL;
        options.preset = options.preset || 'script';
        this.loadAny(url, options, onComplete);
    },


 loadAny (requests, options, onProgress, onComplete) {
        var { options, onProgress, onComplete } = parseParameters(options, onProgress, onComplete);
        
        options.preset = options.preset || 'default';
        requests = Array.isArray(requests) ? requests.concat() : requests;
        // 这里创建了第一个任务
        let task = new Task({input: requests, onProgress, onComplete: asyncify(onComplete), options});
        // 管线开始异步执行任务
        pipeline.async(task);
    },

接下来继续看看pipeline的 async方法:该方法会从pipes遍历管线 并且执行该任务,这里管线有几种:可以在CCAssetManager.js这个文件中找到:


    this._preprocessPipe = preprocess;
    
    this._fetchPipe = fetch;
    
    this._loadPipe = load;

    /**
     * !#en 
     * Normal loading pipeline
     * 
     * !#zh
     * 正常加载管线 加载管线负责下载和解析
     * 
     * @property pipeline
     * @type {Pipeline}
     */
    this.pipeline = pipeline.append(preprocess).append(load);
    
    /**
     * !#en 
     * Fetching pipeline
     * 
     * !#zh
     * 预加载管线只负责下载而不进行解析
     * 
     * @property fetchPipeline
     * @type {Pipeline}
     */
    this.fetchPipeline = fetchPipeline.append(preprocess).append(fetch);

    /**
     * !#en 
     * Url transformer
     * 
     * !#zh
     * Url 转换器 资源解析管线 转换资源路径
     * 
     * @property transformPipeline
     * @type {Pipeline}
     */
    this.transformPipeline = transformPipeline.append(parse).append(combine);

 url转换器该模块做了什么功能:该功能在urlTransformer.js里面

/** 解析资源 */
function parse (task) {

    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') {
            // 强制将item变成一个对象方便下面流程的处理
            item = Object.create(null);
            item[options.__requestType__ || RequestType.UUID] = input[i];
        }
        if (typeof item === 'object') {
            // local options will overlap glabal options
            // item的属性加上 options里的字段
            cc.js.addon(item, options);
            if (item.preset) {
                // 在item加上特定的预设参数
                cc.js.addon(item, cc.assetManager.presets[item.preset]);
            }
            for (var key in item) {
                switch (key) {
                    case RequestType.UUID: 
                        // 将item里面的信息复制到out中去
                        // 请求实际的资源
                        var uuid = out.uuid = decodeUuid(item.uuid);
                        if (bundles.has(item.bundle)) {
                            // 获取到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: 
                        // 当遇到类型为dir的时候证明需要遍历这个文件夹将资源文件填入input 
                        // 查询bundles里面是否存在该bundle
                        if (bundles.has(item.bundle)) {
                            var infos = [];
                            // 将带dir的资源统统装入到infos数组中
                            bundles.get(item.bundle)._config.getDirWithPath(item.dir, item.type, infos);
                            /**
                             * 
                             * infos: {
                            *   ctor: function: 该资源对应的构造函数,
                            *   path: string 例如: effects/builtin-2d-graphics,
                            *   uuid: string 例如: 30682f87-9f0d-4f17-8a44-72863791461b,
                            * }[]
                             * 
                             * 
                             */
                            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});
                            }
                        }
                        /**
                         * 
                         * input: [
                         *      REQUESTTYPE.DIR('effects','materials'),
                         *      // 该文件夹下使用的资源uuid对象
                         *      {
                         *          uuid: string,
                         *          bundle: string,
                         *          ext: string,
                         *          __isNative__: boolean
                         *      },
                         *      {
                         *          uuid: string,
                         *          bundle: string,
                         *          ext: string,
                         *          __isNative__: boolean
                         *      },
                         *      ....
                         * ]
                         * 
                         */

                        // 回收RequestItem
                        out.recycle();
                        out = null;
                        break;
                    case RequestType.PATH: 
                        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:
                        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: 
                        // 如果item里面存在属性url就将url赋值给out
                        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;
        // 将组装出来的请求信息压住到任务的output中供其他管线使用
        task.output.push(out);
        if (!out.uuid && !out.url) throw new Error('Can not parse this input:' + JSON.stringify(item));
    }
    return null;
}

 总结一下parse干了些什么工作:根据input输入的内容填充output中的RequestItem,以供下面的管线使用。这时候的RequestItem中的url不是完整的请求路径,需要经过下一个流水线combine的组合形成完整的请求url路径。

combine流水线:将RequestItem的url拼接成正确的请求路径

/**
 * 组合资源路径 填充RequestItem中的url的资源路径 assets/native/54/54asf-32....png
 * @param  {Task} task
 */
function combine (task) {
    var input = task.output = task.input;
    for (var i = 0; i < input.length; i++) {
        var item = input[i];
        if (item.url) continue;

        var url = '', base = '';
        var config = item.config;
        // config.base: "assets/internal"
        if (item.isNative) {
            // config.nativeBase: "native"
            base = (config && config.nativeBase) ? (config.base + config.nativeBase) : cc.assetManager.generalNativeBase;
        } 
        else {
            // importBase : "import"
            base = (config && config.importBase) ? (config.base + config.importBase) : cc.assetManager.generalImportBase;
        }

        // base形如: “assets/internal/import” or "assets/internal/native"

        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) : '';
            }
        }

        // 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}`;
        }
        // url 形如: assets/internal/import/30/30682f87-9f0d-4f17-8a44-72863791461b.json
        
        item.url = url;
    }
    return null;
}

b: 任务流通:在pipeline的async中调用了一个方法_flow: 递归遍历所有的管道,该管道里面有parse,combine两个管线,parse的task.output -> task.input && task.output = null,这样parse的task的输出成了combine的input,这样完成了任务的流通,这里配了一张图:

2: assetManager.assets是什么时候装入的:先揭晓下答案这个操作发生在资源load阶段,这也是为什么加载进度有时候会倒退的问题,资源是边下载边解析。

3: 三大管线做了哪些工作: 

第一个部分其实已经解决了 transformPipeline这条解析资源路径的管线,接下来来看看 加载管线 也就是pipeline

/**
     * !#en 
     * Normal loading pipeline
     * 
     * !#zh
     * 正常加载管线
     * 
     * @property pipeline
     * @type {Pipeline}
     */
    this.pipeline = pipeline.append(preprocess).append(load);

加载管线时候两个子管线组成分别是:preprocess和load,之前已经说过preprocess是利用transformPipeline来解析资源的路径输出RequestItem -> 流入到load管线开始正式加载资源。该管线包括了下载和解析:这在load.js中可以找到答案

var loadOneAssetPipeline = new Pipeline('loadOneAsset', [
    // 下载流程
    function fetch (task, done) {
        var item = task.output = task.input;
        var { options, isNative, uuid, file } = item;
        var { reload } = options;

        if (file || (!reload && !isNative && assets.has(uuid))) return done();

        // 根据RequestItem加载资源 ,任务的优先级等参数决定下载的顺序
        packManager.load(item, task.options, function (err, data) {
            // 下载完资源后填充 RequestItem的file属性
            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) {
            // texture2D原生资源等等
            parser.parse(id, file, item.ext, options, function (err, asset) {
                if (err) return done(err);
                item.content = asset;
                progress.canInvoke && task.dispatch('progress', ++progress.finish, progress.total, item);
                files.remove(id);
                parsed.remove(id);
                done();
            });
        }
        else {
            // 非原生资源 spriteFrame
            var { uuid } = item;
            if (uuid in exclude) {
                // 再排除的资源列表内
                var { finish, content, err, callbacks } = exclude[uuid];
                progress.canInvoke && task.dispatch('progress', ++progress.finish, progress.total, item);
    
                if (finish || checkCircleReference(uuid, uuid, exclude) ) {
                    content && content.addRef && content.addRef();
                    item.content = content;
                    done(err);
                }
                else {
                    callbacks.push({ done, item });
                }
            }
            else {
                if (!options.reload && assets.has(uuid)) {
                    var asset = assets.get(uuid);
                    if (options.__asyncLoadAssets__ || !asset.__asyncLoadAssets__) {
                        item.content = asset.addRef();
                        progress.canInvoke && task.dispatch('progress', ++progress.finish, progress.total, item);
                        done();
                    }
                    else {
                        loadDepends(task, asset, done, false);
                    }
                }
                else {
                    parser.parse(id, file, 'import', options, function (err, asset) {
                        if (err) return done(err);
                        asset._uuid = uuid;
                        loadDepends(task, asset, done, true);
                    });
                }
            }
        }
    }
]);

首先看下PackManager.load这个下载流程:它其实是依赖于 downloader.download方法完成下载的:

download (id, url, type, options, onComplete) {
        let func = downloaders[type] || downloaders['default'];
        let self = this;
        // if it is downloaded, don't download again
        let file, downloadCallbacks;
        // 下载完的资源直接从资源文件列表中直接获取返回
        if (file = files.get(id)) {
            onComplete(null, file);
        }
        else if (downloadCallbacks = _downloading.get(id)) {
            downloadCallbacks.push(onComplete);
            for (let i = 0, l = _queue.length; i < l; i++) {
                var item = _queue[i];
                if (item.id === id) {
                    var priority = options.priority || 0;
                    if (item.priority < priority) {
                        item.priority = priority;
                        _queueDirty = true;
                    } 
                    return;
                }
            } 
        }
        else {
            // if download fail, should retry
            // 最大下载重试次数
            var maxRetryCount = typeof options.maxRetryCount !== 'undefined' ? options.maxRetryCount : this.maxRetryCount;
            // 最大并发请求数
            var maxConcurrency = typeof options.maxConcurrency !== 'undefined' ? options.maxConcurrency : this.maxConcurrency;
            // 每帧最大请求数量
            var maxRequestsPerFrame = typeof options.maxRequestsPerFrame !== 'undefined' ? options.maxRequestsPerFrame : this.maxRequestsPerFrame;

            function process (index, callback) {
                if (index === 0) {
                    _downloading.add(id, [onComplete]);
                }
                
                if (!self.limited) return func(urlAppendTimestamp(url), options, callback);

                // refresh
                updateTime();

                function invoke () {
                    func(urlAppendTimestamp(url), options, function () {
                        // when finish downloading, update _totalNum
                        _totalNum--;
                        if (!_checkNextPeriod && _queue.length > 0) {
                            callInNextTick(handleQueue, maxConcurrency, maxRequestsPerFrame);
                            _checkNextPeriod = true;
                        }
                        callback.apply(this, arguments);
                    });
                }

                if (_totalNum < maxConcurrency && _totalNumThisPeriod < maxRequestsPerFrame) {
                    invoke();
                    _totalNum++;
                    _totalNumThisPeriod++;
                }
                else {
                    // when number of request up to limitation, cache the rest
                    _queue.push({ id, priority: options.priority || 0, invoke });
                    _queueDirty = true;
    
                    if (!_checkNextPeriod && _totalNum < maxConcurrency) {
                        callInNextTick(handleQueue, maxConcurrency, maxRequestsPerFrame);
                        _checkNextPeriod = true;
                    }
                }
            }

            // when retry finished, invoke callbacks
            // 重试结束后调用回调函数
            function finale (err, result) {
                if (!err) files.add(id, result);
                // 从正在下载的列表移除该id
                var callbacks = _downloading.remove(id);
                for (let i = 0, l = callbacks.length; i < l; i++) {
                    callbacks[i](err, result);
                }
            }
    
            retry(process, maxRetryCount, this.retryInterval, finale);
        }
    }

大部分资源下载都是走的ajax请求:这里我就不贴代码了,大家可以自行找到文件download-file.js看下。

预加载的逻辑先不看了,有时间再补充下。

4: 加载进度会倒退的问题:这个在第二条有体现。

至此关于asssetManager的源码解读就这些了,如果有不同意见请指正谢谢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值