这系列文章会对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、依赖哪些资源、以及加载完成后的对象等等。

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;

Cocos Creator 资源加载流程剖析【一】——cc.loader与加载管线的更多相关文章

  1. Cocos Creator 资源加载流程剖析【二】——Download部分

    Download流程的处理由Downloader这个pipe负责(downloader.js),Downloader提供了各种资源的"下载"方式--即如何获取文件内容,有从网络获取 ...

  2. Cocos Creator 资源加载流程剖析【六】——场景切换流程

    这里讨论场景切换的完整流程,从我们调用了loadScene开始切换场景,到场景切换完成背后发生的事情.整个流程可以分为场景加载和场景切换两部分,另外还简单讨论了场景的预加载. 加载场景的流程 load ...

  3. Cocos Creator 资源加载流程剖析【五】——从编辑器到运行时

    我们在编辑器中看到的资源,在构建之后会进行一些转化,本章将揭开Creator对资源进行的处理. 资源处理的整体规则 首先我们将Creator的开发和运行划分为以下几个场景: 编辑器 当我们将资源放到编 ...

  4. Cocos Creator 资源加载流程剖析【三】——Load部分

    Load流程是整个资源加载管线的最后一棒,由Loader这个pipe负责(loader.js).通过Download流程拿到内容之后,需要对内容做一些"加载"处理.使得这些内容可以 ...

  5. Cocos Creator 资源加载流程剖析【四】——额外流程(MD5 PIPE)

    当我们将游戏构建发布到web平台时,勾选Md5 Cache选项可以开启MD5 Pipe,它的作用是给构建后的资源加上md5后缀,避免浏览器的缓存导致部分资源不是最新,因为使用了md5后缀后,当资源内容 ...

  6. Cocos Creator 资源加载(笔记)

    cc.loader 加载资源动态加载资源要注意两点,一是所有需要通过脚本动态加载的资源,都必须放置在 resources 文件夹或它的子文件夹下.resources 需要在 assets 文件夹中手工 ...

  7. Cocos Creator 新资源管理系统剖析

    目录 1.资源与构建 1.1 creator资源文件基础 1.2 资源构建 1.2.1 图片.图集.自动图集 1.2.2 Prefab与场景 1.2.3 资源文件合并规则 2. 理解与使用 Asset ...

  8. Cocos Creator 源码解读:引擎启动与主循环

    前言 预备 不知道你有没有想过,假如把游戏世界比作一辆汽车,那么这辆"汽车"是如何启动,又是如何持续运转的呢? 如题,本文的内容主要为 Cocos Creator 引擎的启动流程和 ...

  9. cocos creator主程入门教程(三)—— 资源管理

    五邑隐侠,本名关健昌,10年游戏生涯,现隐居五邑.本系列文章以TypeScript为介绍语言. 在初识篇,我介绍过怎样加载prefab.cocos提供了一系列的加载接口,包括cc.loader.loa ...

随机推荐

  1. SSM-SpringMVC-21:SpringMVC中处理器方法之返回值Object篇

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 今天要记录的是处理方法,返回值为Object的那种,我给它分了一下类: 1.返回值为Object数值(例如1) ...

  2. Spring Boot Hikari

    Guys, I got the following properties to work, kind of. The following creates 2 pools. One connection ...

  3. Java描述数据结构之链表的增删改查

    链表是一种常见的基础数据结构,它是一种线性表,但在内存中它并不是顺序存储的,它是以链式进行存储的,每一个节点里存放的是下一个节点的"指针".在Java中的数据分为引用数据类型和基础 ...

  4. linux常用命令:创建文件和文件夹

    1.首先说一下touch 创建文件的命令,touch可以用于创建二进制文件,用法非常简单.用法:touch+文件名,touch与文件名之间一定要有空格.图中先用之前分享过的命令来查看一下/目录下面有多 ...

  5. RabbitMQ分布式集群架构和高可用性(HA)

    (一) 功能和原理 设计集群的目的 允许消费者和生产者在RabbitMQ节点崩溃的情况下继续运行 通过增加更多的节点来扩展消息通信的吞吐量 1 集群配置方式 RabbitMQ可以通过三种方法来部署分布 ...

  6. 树莓派.安装Firefox浏览器

    sudo apt-get install firefox-esr 要做全屏效果的话, 可以加装插件FF Fullscreen 插件地址: https://addons.mozilla.org/en-U ...

  7. 把List<string>集合,编程string,并以“,”号分割

    List<int> roleIdList = GetRoleIdList(user.ID); string roleIdsStr = ""; if (roleIdLis ...

  8. auth.go

    ), fmt.Errorf("invalid permission type: %s", s) } type authenticator struct {     conn *gr ...

  9. BZOJ_3238_[Ahoi2013]差异_后缀自动机

    BZOJ_3238_[Ahoi2013]差异_后缀自动机 Description Input 一行,一个字符串S Output 一行,一个整数,表示所求值 Sample Input cacao Sam ...

  10. BZOJ_3012_[Usaco2012 Dec]First!_trie树+拓扑排序

    BZOJ_3012_[Usaco2012 Dec]First!_trie树+拓扑排序 题意: 给定n个总长不超过m的互不相同的字符串,现在你可以任意指定字符之间的大小关系.问有多少个串可能成为字典序最 ...