最近用到资源加载的问题:加载的资源进度条倒退问题,现在只是用临时的解决方案 - 加载进度如果出现会倒退的问题还是用原来的数值。抽时间看了下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的源码解读就这些了,如果有不同意见请指正谢谢