这系列文章会对Cocos Creator的资源加载和管理进行深刻的剖析。主要包含如下内容:javascript
前面4章节介绍了完整的资源加载流程以及资源管理,以及如何自定义这个加载流程(有时候咱们须要加载一些特殊类型的资源)。“从编辑器到运行时”介绍了咱们在编辑器中编辑的场景、Prefab等资源是如何序列化到磁盘,打包发布以后又是如何被加载到游戏中。html
在开始以前咱们须要解决这几个问题:java
引擎的代码大致分为js和原生c++ 两种类型,在web平台上不使用任何 c++ 代码,而是一个基于webgl编写的渲染底层。而在移动平台上仍然使用 c++ 的底层,经过jsb将原生的接口暴露给上层的js。在引擎安装目录下的resources/engine下放着引擎的全部js代码。而原生c++ 代码放在引擎安装目录下的resources/cocos2d-x目录下。咱们能够在这两个目录下查看代码。这系列文章中咱们要查看的代码位于引擎安装目录下的resources/engine/cocos2d/core/load-pipeline目录下。c++
JS的调试很是简单,咱们能够在Chrome浏览器运行程序,按F12进入调试模式,经过ctrl + p快捷键能够根据文件名搜索源码,进行断点调试。具体的各类调试技巧可参考如下几个教程。web
原平生台的调试也能够用Chrome,官方的文档介绍了如何调试原生普通的JS代码。至于原平生台的C++ 代码调试,能够在Windows上使用Visual Studio调试,也能够在Mac上使用XCode调试。chrome
首先咱们从总体上观察CCLoader大体的类结构,这个密密麻麻的图估计没有人会仔细看,因此这里简单介绍一下:json
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提供了如下加载资源的接口:浏览器
loadRes是咱们最经常使用的一个接口,该函数主要作了3个事情:缓存
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是如何处理资源的,具体可参考《从编辑器到运行时》一章。
在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是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;