zoukankan      html  css  js  c++  java
  • Cocos Creator 资源加载流程剖析【一】——cc.loader与加载管线

    这系列文章会对Cocos Creator的资源加载和管理进行深入的剖析。主要包含以下内容:

    • cc.loader与加载管线
    • Download部分
    • Load部分
    • 额外流程(MD5 Pipe)
    • 从编辑器到运行时
    • 场景切换流程

    前面4章节介绍了完整的资源加载流程以及资源管理,以及如何自定义这个加载流程(有时候我们需要加载一些特殊类型的资源)。“从编辑器到运行时”介绍了我们在编辑器中编辑的场景、Prefab等资源是如何序列化到磁盘,打包发布之后又是如何被加载到游戏中。


    准备工作

    在开始之前我们需要解决这几个问题:

    • 如何阅读代码?

    引擎的代码大体分为js和原生c++ 两种类型,在web平台上不使用任何 c++ 代码,而是一个基于webgl编写的渲染底层。而在移动平台上仍然使用 c++ 的底层,通过jsb将原生的接口暴露给上层的js。在引擎安装目录下的resources/engine下放着引擎的所有js代码。而原生c++ 代码放在引擎安装目录下的resources/cocos2d-x目录下。我们可以在这两个目录下查看代码。这系列文章中我们要查看的代码位于引擎安装目录下的resources/engine/cocos2d/core/load-pipeline目录下。

    • 如何调试代码?

    JS的调试非常简单,我们可以在Chrome浏览器运行程序,按F12进入调试模式,通过ctrl + p快捷键可以根据文件名搜索源码,进行断点调试。具体的各种调试技巧可参考以下几个教程。

    原生平台的调试也可以用Chrome,官方的文档介绍了如何调试原生普通的JS代码。至于原生平台的C++ 代码调试,可以在Windows上使用Visual Studio调试,也可以在Mac上使用XCode调试。


    框架结构

    首先我们从整体上观察CCLoader大致的类结构,这个密密麻麻的图估计没有人会仔细看,所以这里简单介绍一下:

    • 我们的CCLoader继承于Pipeline,CCLoader提供了友好的资源管理接口(加载、获取、释放)以及一些辅助接口(如自动释放、对Pipeline的修改)。
    • Pipeline中主要包含了多个Pipe和多个LoadingItems,这里实现了一个Pipe到Pipe衔接流转的过程,以及Pipe和LoadingItems的管理接口。
    • Pipe有多种子类,每一种Pipe都会对资源进行特定的加工,后面会对每一种Pipe都作详细介绍。
    • LoadingItems为一个加载队列,继承于CallbackInvoker,管理着LoadingItem(注意没有复数),一个LoadingItem就是资源从开始加载到加载完成的上下文。这里说的上下文,指的是与加载该资源相关的变量的集合,比如当前加载的状态、url、依赖哪些资源、以及加载完成后的对象等等。

    image

    CocosCreator2.x和1.x版本对比,整个加载的流程没有太大的变化,主要的变化是引入了FontLoader,将Font初始化的逻辑从Downloader转移到了Loader这个Pipe中。将JSB的部分分开,在编译时彻底根据不同的平台编译不同的js,而不是在一个js中使用条件判断当前是什么平台来执行对应的代码。其他优化了一些写法,比如cc.Class.inInstanceOf调整为instanceof,JS.getClassName、cc.isChildClassOf等方法移动到js这个模块中。

    资源加载

    CCLoader提供了多种加载资源的接口,要加载的资源必须放到resources目录下,我们在加载资源的时候,除了要加载的资源url和完成回调,最好将type参数传入,这是一个良好的习惯。CCLoader提供了以下加载资源的接口:

    • load(resources, progressCallback, completeCallback)
    • loadRes(url, type, progressCallback, completeCallback)
    • loadResArray(urls, type, progressCallback, completeCallback)
    • loadResDir(url, type, progressCallback, completeCallback)

    loadRes是我们最常用的一个接口,该函数主要做了3个事情:

    • 调用_getResUuid查询uuid,该方法会调用AssetTable的getUuid方法查询资源的uuid。从网络上加载的资源以及SD卡中我们存储的资源,Creator并没有为它们生成uuid。所以这些不是在Creator项目中生成的资源不能使用loadRes来加载
    • 调用this.load方法加载资源。
    • 在加载完成后,该资源以及其引用的资源都会被标记为禁止自动释放(在场景切换的时候,Creator会自动释放下个场景不使用的资源)。
    proto.loadRes = function (url, type, progressCallback, completeCallback) {
        var args = this._parseLoadResArgs(type, progressCallback, completeCallback);
        type = args.type;
        progressCallback = args.onProgress;
        completeCallback = args.onComplete;
        var self = this;
        var uuid = self._getResUuid(url, type);
        if (uuid) {
            this.load(
                {
                    type: 'uuid',
                    uuid: uuid
                },
                progressCallback,
                function (err, asset) {
                    if (asset) {
                        // 禁止自动释放资源
                        self.setAutoReleaseRecursively(uuid, false);
                    }
                    if (completeCallback) {
                        completeCallback(err, asset);
                    }
                }
            );
        }
        else {
            self._urlNotFound(url, type, completeCallback);
        }
    };
    

    无论调用哪个接口,最后都会走到load函数,load函数做了几个事情,首先是对输入的参数进行处理,以满足其他资源加载接口的调用,所有要加载的资源最后会被添加到_sharedResources中(不论该资源是否已加载,如果已加载会push它的item,未加载会push它的res对象,res对象是通过getResWithUrl方法从AssetLibrary中查询出来的,AssetLibrary在后面的章节中会详细介绍)。

    load和其它接口的最大区别在于,load可以用于加载绝对路径的资源(比如一个sd卡的绝对路径、或者网络上的一个url),而loadRes等只能加载resources目录下的资源。

    proto.load = function(resources, progressCallback, completeCallback) {
        // 下面这几段代码对输入的参数进行了处理,保证了load函数的各种重载写法能被正确识别
        // progressCallback是可选的,可以只传入resources和completeCallback
        if (completeCallback === undefined) {
            completeCallback = progressCallback;
            progressCallback = this.onProgress || null;
        }
    
        // 检测是否为单个资源的加载
        var self = this;
        var singleRes = false;
        if (!(resources instanceof Array)) {
            singleRes = true;
            resources = resources ? [resources] : [];
        }
    
        // 将待加载的资源放到_sharedResources数组中
        _sharedResources.length = 0;
        for (var i = 0; i < resources.length; ++i) {
            var resource = resources[i];
            // 前向兼容 {id: 'http://example.com/getImageREST?file=a.png', type: 'png'} 这种写法
            if (resource && resource.id) {
                cc.warnID(4920, resource.id);
                if (!resource.uuid && !resource.url) {
                    resource.url = resource.id;
                }
            }
            // 支持以下格式的写法
            // 1. {url: 'http://example.com/getImageREST?file=a.png', type: 'png'}
            // 2. 'http://example.com/a.png'
            // 3. 'a.png'
            var res = getResWithUrl(resource);
            if (!res.url && !res.uuid)
                continue;
                
            // 如果是已加载过的资源这里会把它取出
            var item = this._cache[res.url];
            _sharedResources.push(item || res);
        }
    
        // 创建一个LoadingItems加载队列,在所有资源加载完成后的下一帧执行完成回调
        var queue = LoadingItems.create(this, progressCallback, function (errors, items) {
            callInNextTick(function () {
                if (completeCallback) {
                    if (singleRes) {
                        let id = res.url;
                        completeCallback.call(self, items.getError(id), items.getContent(id));
                    }
                    else {
                        completeCallback.call(self, errors, items);
                    }
                    completeCallback = null;
                }
    
                if (CC_EDITOR) {
                    for (let id in self._cache) {
                        if (self._cache[id].complete) {
                            self.removeItem(id);
                        }
                    }
                }
                items.destroy();
            });
        });
        // 初始化队列
        LoadingItems.initQueueDeps(queue);
        // 真正的启动加载管线
        queue.append(_sharedResources);
        _sharedResources.length = 0;
    };
    

    初始化_sharedResources之后,开始创建一个LoadingItems,将调用queue.append将_sharedResources追加到LoadingItems中。特别需要注意的地方是,我们的加载完成回调,至少会在下一帧才执行,因为这里用了一个callInNextTick包裹了传入的completeCallback。

    LoadingItems.create方法主要的职责包含LoadingItems的创建(使用对象池进行复用),绑定onProgress和onComplete回调到queue对象中(创建出来的LoadingItems类实例)。

    queue.append完成了资源加载的准备和启动,首先遍历要加载的所有资源(urlList),检查已在队列中的资源对象,如果已经加载完成或者为循环引用对象则当做加载完成处理,否则在该资源的加载队列中添加监听,在资源加载完成后执行self.itemComplete(item.id)。

    如果是一个全新的资源,则调用createItem创建这个资源的item,把item放到this.map和accepted数组中。综上,如果我们使用CCLoader去加载一个已加载完成的资源,也会在下一帧才得到回调。

    proto.append = function (urlList, owner) {
        if (!this.active) {
            return [];
        }
        if (owner && !owner.deps) {
            owner.deps = [];
        }
    
        this._appending = true;
        var accepted = [], i, url, item;
        for (i = 0; i < urlList.length; ++i) {
            url = urlList[i];
    
            // 已经在另一个LoadingItems队列中了,url对象就是实际的item对象
            // 在load方法中,如果已加载或正在加载,会取出_cache[res.url]添加到urlList
            if (url.queueId && !this.map[url.id]) {
                this.map[url.id] = url;
                // 将url添加到owner的deps数组中,以便于检测循环引用
                owner && owner.deps.push(url);
                // 已加载完成或循环引用(在递归该资源的依赖时,发现了该资源自己的id,owner.id)
                if (url.complete || checkCircleReference(owner, url)) {
                    this.totalCount++;
                    this.itemComplete(url.id);
                    continue;
                }
                // 还未加载完成,需要等待其加载完成
                else {
                    var self = this;
                    var queue = _queues[url.queueId];
                    if (queue) {
                        this.totalCount++;
                        LoadingItems.registerQueueDep(owner || this._id, url.id);
                        // 已经在其它队列中加载了,监听那个队列该资源加载完成的事件即可
                        // 如果加载失败,错误会记录在item.error中
                        queue.addListener(url.id, function (item) {
                            self.itemComplete(item.id);
                        });
                    }
                    continue;
                }
            }
            // 队列中的新item,从未加载过
            if (isIdValid(url)) {
                item = createItem(url, this._id);
                var key = item.id;
                // 不存在重复的url
                if (!this.map[key]) {
                    this.map[key] = item;
                    this.totalCount++;
                    // 将item添加到owner的deps数组中,以便于检测循环引用
                    owner && owner.deps.push(item);
                    LoadingItems.registerQueueDep(owner || this._id, key);
                    accepted.push(item);
                }
            }
        }
        this._appending = false;
    
        // 全部完成则手动结束
        if (this.completedCount === this.totalCount) {
            this.allComplete();
        }
        else {
            // 开始加载本次需要加载的资源(accepted数组)
            this._pipeline.flowIn(accepted);
        }
        return accepted;
    };
    

    如果全部资源已经加载完成,则执行this.allComplete,否则调用this._pipeline.flowIn(accepted),启动由本队列进行加载的部分资源。

    基本上所有的资源都会有一个uuid,Creator会为它生成一个json文件,一般都是先加载其json文件,再进一步加载其依赖资源。CCLoader和LoadingItems本身并不处理这些依赖资源的加载,依赖加载是由UuidLoader这个加载器进行加载的。这个设计看上去会导致的一个问题就是加载大部分的资源都会有2个io操作,一个是json文件的加载,一个是raw资源的加载。Creator是如何处理资源的,具体可参考《从编辑器到运行时》一章。

    Pipeline的流转

    在LoadingItems的append方法中,调用了flowIn启动了Pipeline,传入的accepted数组为新加载的资源——即未加载完成,也不处于加载中的资源。

    Pipeline的flowIn方法中获取this._pipes的第一个pipe,遍历所有的item,调用flow传入该pipe来处理每一个item。如果获取不到第一个pipe,则调用flowOut来处理所有的item,直接将item从Pipeline中流出。

    默认情况下,CCLoader初始化有3个Pipe,分别是AssetLoader(获取资源的详细信息以便于决定后续使用何种方式处理)、Downloader(处理了iOS、Android、Web等平台以及各种类型资源的下载——即读取文件)、Loader(对已下载的资源进行加载解析处理,使游戏内可以直接使用)。

    proto.flowIn = function (items) {
        var i, pipe = this._pipes[0], item;
        if (pipe) {
            // 第一步先Cache所有的item,以防止重复加载相同的item!!!
            for (i = 0; i < items.length; i++) {
                item = items[i];
                this._cache[item.id] = item;
            }
            for (i = 0; i < items.length; i++) {
                item = items[i];
                flow(pipe, item);
            }
        }
        else {
            for (i = 0; i < items.length; i++) {
                this.flowOut(items[i]);
            }
        }
    };
    

    flow方法主要的职责包含检查item处理的状态,如果有异常进行异常处理,调用pipe的handle方法对item进行处理,衔接下一个pipe,如果没有下一个pipe则调用Pipeline.flowOut对item进行流出。

    function flow (pipe, item) {
        var pipeId = pipe.id;
        var itemState = item.states[pipeId];
        var next = pipe.next;
        var pipeline = pipe.pipeline;
    
        // 出错或已在处理中则不需要进行处理
        if (item.error || itemState === ItemState.WORKING || itemState === ItemState.ERROR) {
            return;
        // 已完成则驱动下一步
        } else if (itemState === ItemState.COMPLETE) {
            if (next) {
                flow(next, item);
            }
            else {
                pipeline.flowOut(item);
            }
        } else {
            // 开始处理
            item.states[pipeId] = ItemState.WORKING;
            // pipe.handle【可能】是异步的,传入匿名函数在pipe执行完时调用
            var result = pipe.handle(item, function (err, result) {
                if (err) {
                    item.error = err;
                    item.states[pipeId] = ItemState.ERROR;
                    pipeline.flowOut(item);
                }
                else {
                    // result可以为null,这意味着该pipe没有result
                    if (result) {
                        item.content = result;
                    }
                    item.states[pipeId] = ItemState.COMPLETE;
                    if (next) {
                        flow(next, item);
                    }
                    else {
                        pipeline.flowOut(item);
                    }
                }
            });
            // 如果返回了一个Error类型的result,则要进行记录,修改item状态,并调用flowOut流出item
            if (result instanceof Error) {
                item.error = result;
                item.states[pipeId] = ItemState.ERROR;
                pipeline.flowOut(item);
            }
            // 如果返回了非undefined的结果
            else if (result !== undefined) {
                // 意为着这个pipe没有result
                if (result !== null) {
                    item.content = result;
                }
                item.states[pipeId] = ItemState.COMPLETE;
                if (next) {
                    flow(next, item);
                }
                else {
                    pipeline.flowOut(item);
                }
            }
            // 其它情况为返回了undefined,这意味着这个pipe是一个异步的pipe,且启动handle的时候没有出现错误,我们传入的回调会被执行,在回调中驱动下一个pipe或结束Pipeline。
        }
    }
    

    flowOut方法流出资源,如果item在Pipeline处理中出现了错误,会被删除。否则会保存该item到this._cache中,this._cache中是缓存所有已加载资源的容器。最后调用LoadingItems.itemComplete(item),这个方法会驱动onProgress、onCompleted等方法的执行。

    proto.flowOut = function (item) {
        if (item.error) {
            delete this._cache[item.id];
        }
        else if (!this._cache[item.id]) {
            this._cache[item.id] = item;
        }
        item.complete = true;
        LoadingItems.itemComplete(item);
    };
    

    在每一个item加载结束后,都会执行LoadingItems.itemComplete进行收尾。

    proto.itemComplete = function (id) {
        var item = this.map[id];
        if (!item) {
            return;
        }
    
        // 错误处理
        var errorListId = this._errorUrls.indexOf(id);
        if (item.error && errorListId === -1) {
            this._errorUrls.push(id);
        }
        else if (!item.error && errorListId !== -1) {
            this._errorUrls.splice(errorListId, 1);
        }
    
        this.completed[id] = item;
        this.completedCount++;
    
        // 遍历_queueDeps,找到所有依赖该资源的queue,将该资源添加到对应queue的completed数组中
        LoadingItems.finishDep(item.id);
        // 进度回调
        if (this.onProgress) {
            var dep = _queueDeps[this._id];
            this.onProgress(dep ? dep.completed.length : this.completedCount, dep ? dep.deps.length : this.totalCount, item);
        }
        // 触发该id加载结束的事件,所有依赖该资源的LoadingItems对象会触发该事件
        this.invoke(id, item);
        // 移除该id的所有监听回调
        this.removeAll(id);
    
        // 如果全部加载完成了,会执行allComplete,驱动onComplete回调
        if (!this._appending && this.completedCount >= this.totalCount) {
            // console.log('===== All Completed ');
            this.allComplete();
        }
    };
    

    AssetLoader

    AssetLoader是Pipeline的第一个Pipe,这个Pipe的职责是进行初始化,从cc.AssetLibrary中取出该资源的完整信息,获取该资源的类型,对rawAsset类型进行设置type,方便后面的pipe执行不同的处理,而非rawAsset则执行callback进入下一个Pipe处理。其实AssetLoader在这里的作用看上去并不大,因为基本上所有的资源走到这里都是直接执行回调或返回,从Creator最开始的代码来看,默认只有Downloader和Loader两个Pipe。且我在调试的时候注释了Pipeline初始化AssetLoader的地方,在一个开发到后期的项目中测试发现对资源加载这块毫无影响。

    我们调用loadRes加载的资源都会被转为uuid,所以都会通过cc.AssetLibrary.queryAssetInfo查询到对应的信息。然后执行item.type = 'uuid',对应的raw类型资源,如纹理会在UuidLoader中进行依赖加载的处理,详见Load部分。

    var AssetLoader = function (extMap) {
        this.id = ID;
        this.async = true;
        this.pipeline = null;
    };
    AssetLoader.ID = ID;
    
    var reusedArray = [];
    AssetLoader.prototype.handle = function (item, callback) {
        var uuid = item.uuid;
        if (!uuid) {
            return !!item.content ? item.content : null;
        }
    
        var self = this;
        cc.AssetLibrary.queryAssetInfo(uuid, function (error, url, isRawAsset) {
            if (error) {
                callback(error);
            }
            else {
                item.url = item.rawUrl = url;
                item.isRawAsset = isRawAsset;
                if (isRawAsset) {
                    /* 基本上raw类型的资源也不会走到这个分支,经过各种调试都没有让程序运行到这个分支下,
                    因为所有的资源在加载的时候都是先获取其uuid进行加载的。而没有uuid的情况基本在这个函数的第一行判断uuid的时候就返回了。
                    
                    我还尝试了直接用cc.loader.load加载resources的资源,直接传入resources下的文件会报路径错误。
                    提示的错误类似 http://localhost:7456/loadingBar/image.png 404错误。
                    正确的路径应该是在res/import/...下的,使用使用cc.url.raw可以获取到正确的路径。
                    我将一个纹理修改为RAW类型资源进行加载,并使用cc.url.raw进行加载,直接在函数开始的uuid判断这里返回了。
                    
                    另一个尝试是加载网络中的资源,然而都在函数开始的uuid判断处返回了。
                    
                    所以这段代码应该是被废弃的,不被维护的代码。*/
                    var ext = Path.extname(url).toLowerCase();
                    if (!ext) {
                        callback(new Error(cc._getError(4931, uuid, url)));
                        return;
                    }
                    ext = ext.substr(1);
                    var queue = LoadingItems.getQueue(item);
                    reusedArray[0] = {
                        queueId: item.queueId,
                        id: url,
                        url: url,
                        type: ext,
                        error: null,
                        alias: item,
                        complete: true
                    };
                    if (CC_EDITOR) {
                        self.pipeline._cache[url] = reusedArray[0];
                    }
                    queue.append(reusedArray);
                    // 传递给特定type的Downloader
                    item.type = ext;
                    callback(null, item.content);
                }
                else {
                    item.type = 'uuid';
                    callback(null, item.content);
                }
            }
        });
    };
    
    Pipeline.AssetLoader = module.exports = AssetLoader;
    
  • 相关阅读:
    学习Asp.Net经常会用到的函数集
    string.Format 格式化时间,货币
    sqlite相关
    sql常用判断语句
    ToString() 格式 用法大全 保留 两位 小数
    一些常用短代码,页面缓存啥的
    判断iE并创建A标签
    Java学习之数组1(1.数组的声明;2.元素为引用数据类型的数组;3.关于main方法里的String[] args;4.数组排序;5.数3退1 数组算法,(用数组模拟链表);6数组查找之二分法;7数组的拷贝)
    Java学习之异常处理()
    Java学习之容器上(Collection接口常用方法,Iterator接口,使用foreach循环遍历Collection集合元素,Set集合通用知识(Hashset类,hashcode()与LinkedHashSet类))
  • 原文地址:https://www.cnblogs.com/ybgame/p/10576884.html
Copyright © 2011-2022 走看看