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快捷键可以根据文件名搜索源码,进行断点调试。具体的各种调试技巧可参考以下几个教程。

  • https://juejin.im/entry/5804669f570c35006c828548
  • http://wiki.jikexueyuan.com/project/chrome-devtools/debugging-javascript.html
  • https://developers.google.com/web/tools/chrome-devtools/javascript/

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

  • https://docs.cocos.com/creator/manual/zh/publish/debug-jsb.html
  • https://docs.cocos.com/creator/manual/zh/publish/debug-native.html

框架结构

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

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

 

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和completeCallbackif (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]添加到urlListif (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;// 不存在重复的urlif (!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没有resultif (result) {item.content = result;}item.states[pipeId] = ItemState.COMPLETE;if (next) {flow(next, item);}else {pipeline.flowOut(item);}}});// 如果返回了一个Error类型的result,则要进行记录,修改item状态,并调用flowOut流出itemif (result instanceof Error) {item.error = result;item.states[pipeId] = ItemState.ERROR;pipeline.flowOut(item);}// 如果返回了非undefined的结果else if (result !== undefined) {// 意为着这个pipe没有resultif (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的Downloaderitem.type = ext;callback(null, item.content);}else {item.type = 'uuid';callback(null, item.content);}}});
};Pipeline.AssetLoader = module.exports = AssetLoader;


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部