【本文参与征文活动】
在loadOneAssetPipeline中,资源会经过fetch和parse两个管线进行处理,fetch负责下载而parse负责解析资源,并实例化资源对象。在parse方法中调用了parser.parse将文件内容传入,解析成对应的Asset对象,并返回。
Web平台解析
Web平台下的parser.parse主要做的是对解析中的文件的管理,为解析中、解析完的文件维护一个列表,避免重复解析。同时维护了解析完成后的回调列表,而真正的解析方法在parsers数组中。
parse (id, file, type, options, onComplete) {
let parsedAsset, parsing, parseHandler;
if (parsedAsset = parsed.get(id)) {
onComplete(null, parsedAsset);
}
else if (parsing = _parsing.get(id)){
parsing.push(onComplete);
}
else if (parseHandler = parsers[type]){
_parsing.add(id, [onComplete]);
parseHandler(file, options, function (err, data) {
if (err) {
files.remove(id);
}
else if (!isScene(data)){
parsed.add(id, data);
}
let callbacks = _parsing.remove(id);
for (let i = 0, l = callbacks.length; i < l; i++) {
callbacks[i](err, data);
}
});
}
else {
onComplete(null, file);
}
}
parsers映射了各种类型文件的解析方法,下面以图片和普通的asset资源为例:
注意:在parseImport方法中,反序列化方法会将资源的依赖放到asset.__depends__中,结构为数组,数组中每个对象包含3个字段,资源id uuid、owner 对象、prop 属性。比如一个Prefab资源,下面有2个节点,都引用了同一个资源,depends列表需要为这两个节点对象分别记录一条依赖信息 [{uuid:xxx, owner:1, prop:tex}, {uuid:xxx, owner:2, prop:tex}]
// 映射图片格式到解析方法
var parsers = {
'.png' : parser.parseImage,
'.jpg' : parser.parseImage,
'.bmp' : parser.parseImage,
'.jpeg' : parser.parseImage,
'.gif' : parser.parseImage,
'.ico' : parser.parseImage,
'.tiff' : parser.parseImage,
'.webp' : parser.parseImage,
'.image' : parser.parseImage,
'.pvr' : parser.parsePVRTex,
'.pkm' : parser.parsePKMTex,
// Audio
'.mp3' : parser.parseAudio,
'.ogg' : parser.parseAudio,
'.wav' : parser.parseAudio,
'.m4a' : parser.parseAudio,
// plist
'.plist' : parser.parsePlist,
'import' : parser.parseImport
};
// 图片并不会解析成Asset对象,而是解析成对应的图片对象
parseImage (file, options, onComplete) {
if (capabilities.imageBitmap && file instanceof Blob) {
let imageOptions = {};
imageOptions.imageOrientation = options.__flipY__ ? 'flipY' : 'none';
imageOptions.premultiplyAlpha = options.__premultiplyAlpha__ ? 'premultiply' : 'none';
createImageBitmap(file, imageOptions).then(function (result) {
result.flipY = !!options.__flipY__;
result.premultiplyAlpha = !!options.__premultiplyAlpha__;
onComplete && onComplete(null, result);
}, function (err) {
onComplete && onComplete(err, null);
});
}
else {
onComplete && onComplete(null, file);
}
},
// Asset对象的解析,通过deserialize实现,大致流程是解析json然后找到对应的class,并调用对应class的_deserialize方法拷贝数据、初始化变量,并将依赖资源放到asset.__depends
parseImport (file, options, onComplete) {
if (!file) return onComplete && onComplete(new Error('Json is empty'));
var result, err = null;
try {
result = deserialize(file, options);
}
catch (e) {
err = e;
}
onComplete && onComplete(err, result);
},
原生平台解析
在原生平台下,jsb-loader.js中重新注册了各种资源的解析方法:
parser.register({
'.png' : downloader.downloadDomImage,
'.binary' : parseArrayBuffer,
'.txt' : parseText,
'.plist' : parsePlist,
'.font' : loadFont,
'.ExportJson' : parseJson,
...
});
图片的解析方法竟然是downloader.downloadDomImage?跟踪原生平台调试了一下,确实是调用的这个方法,创建了Image对象并指定src来加载图片,这种方式加载本地磁盘的图片也是可以的,但纹理对象又是如何创建的呢?通过Texture2D对应的json文件,creator在加载真正的原生纹理之前,就已经创建好了Texture2D这个Asset对象,而在加载完原生图片资源后,会将Image对象设置为Texture2D对象的_nativeAsset,在这个属性的set方法中,会调用initWithData或initWithElement,这里才真正使用纹理数据创建了用于渲染的纹理对象。
var Texture2D = cc.Class({
name: 'cc.Texture2D',
extends: require('../assets/CCAsset'),
mixins: [EventTarget],
properties: {
_nativeAsset: {
get () {
// maybe returned to pool in webgl
return this._image;
},
set (data) {
if (data._data) {
this.initWithData(data._data, this._format, data.width, data.height);
}
else {
this.initWithElement(data);
}
},
override: true
},
而对于parseJson、parseText、parseArrayBuffer等实现,这里只是简单地调用了文件系统读取文件而已。像一些拿到文件内容之后,需要进一步解析才能使用的资源呢?比如模型、骨骼等资源依赖二进制的模型数据,这些数据的解析在哪里呢?没错,跟上面的Texture2D一样,都是放在对应的Asset资源本身,有些在_nativeAsset字段的setter回调中初始化,而有些会在真正使用这个资源时才惰性地进行初始化。
// 在jsb-loader.js文件中
function parseText (url, options, onComplete) {
readText(url, onComplete);
}
function parseArrayBuffer (url, options, onComplete) {
readArrayBuffer(url, onComplete);
}
function parseJson (url, options, onComplete) {
readJson(url, onComplete);
}
// 在jsb-fs-utils.js文件中
readText (filePath, onComplete) {
fsUtils.readFile(filePath, 'utf8', onComplete);
},
readArrayBuffer (filePath, onComplete) {
fsUtils.readFile(filePath, '', onComplete);
},
readJson (filePath, onComplete) {
fsUtils.readFile(filePath, 'utf8', function (err, text) {
var out = null;
if (!err) {
try {
out = JSON.parse(text);
}
catch (e) {
cc.warn('Read json failed: ' + e.message);
err = new Error(e.message);
}
}
onComplete && onComplete(err, out);
});
},
像图集、Prefab这些资源又是怎么初始化的呢?Creator还是使用parseImport方法进行解析,因为这些资源对应的类型是import
,原生平台下并没有覆盖这种类型对应的parse函数,而这些资源会直接反序列化成可用的Asset对象。
依赖加载
creator将资源分为两大类,普通资源和原生资源,普通资源包括cc.Asset及其子类,如cc.SpriteFrame、cc.Texture2D、cc.Prefab等等。原生资源包括各种格式的纹理、音乐、字体等文件,在游戏中我们无法直接使用这些原生资源,而是需要让creator将他们转换成对应的cc.Asset对象之后才能使用。
在creator中,一个Prefab可能会依赖很多资源,这些依赖也可以分为普通依赖和原生资源依赖,creator的cc.Asset提供了_parseDepsFromJson
和_parseNativeDepFromJson
方法来检查资源的依赖。loadDepends通过getDepends方法搜集了资源的依赖。
loadDepends创建了一个子任务来负责依赖资源的加载,并调用pipeline执行加载,实际上无论有无依赖需要加载,都会执行这段逻辑,加载完成后执行以下重要逻辑:
- 初始化assset:在依赖加载完成后,将依赖的资源赋值到asset对应的属性后调用asset.onLoad
- 将资源对应的files和parsed缓存移除,并缓存资源到assets中(如果是场景的话,不会缓存)
- 执行repeatItem.callbacks列表中的回调(在loadDepends的开头构造,默认记录传入的done方法)
// 加载指定asset的依赖项
function loadDepends (task, asset, done, init) {
var item = task.input, progress = task.progress;
var { uuid, id, options, config } = item;
var { __asyncLoadAssets__, cacheAsset } = options;
var depends = [];
// 增加引用计数来避免加载依赖的过程中资源被释放,调用getDepends获取依赖资源
asset.addRef && asset.addRef();
getDepends(uuid, asset, Object.create(null), depends, false, __asyncLoadAssets__, config);
task.dispatch('progress', ++progress.finish, progress.total += depends.length, item);
var repeatItem = task.options.__exclude__[uuid] = { content: asset, finish: false, callbacks: [{ done, item }] };
let subTask = Task.create({
input: depends,
options: task.options,
onProgress: task.onProgress,
onError: Task.prototype.recycle,
progress,
onComplete: function (err) {
// 在所有依赖项加载完成之后回调
asset.decRef && asset.decRef(false);
asset.__asyncLoadAssets__ = __asyncLoadAssets__;
repeatItem.finish = true;
repeatItem.err = err;
if (!err) {
var assets = Array.isArray(subTask.output) ? subTask.output : [subTask.output];
// 构造一个map,记录uuid到asset的映射
var map = Object.create(null);
for (let i = 0, l = assets.length; i < l; i++) {
var dependAsset = assets[i];
dependAsset && (map[dependAsset instanceof cc.Asset ? dependAsset._uuid + '@import' : uuid + '@native'] = dependAsset);
}
// 调用setProperties将对应的依赖资源设置到asset的成员变量中
if (!init) {
if (asset.__nativeDepend__ && !asset._nativeAsset) {
var missingAsset = setProperties(uuid, asset, map);
if (!missingAsset) {
try {
asset.onLoad && asset.onLoad();
}
catch (e) {
cc.error(e.message, e.stack);
}
}
}
}
else {
var missingAsset = setProperties(uuid, asset, map);
if (!missingAsset) {
try {
asset.onLoad && asset.onLoad();
}
catch (e) {
cc.error(e.message, e.stack);
}
}
files.remove(id);
parsed.remove(id);
cache(uuid, asset, cacheAsset !== undefined ? cacheAsset : cc.assetManager.cacheAsset);
}
subTask.recycle();
}
// 这个repeatItem可能有很多个地方都加载了它,要通知所有回调加载完成
var callbacks = repeatItem.callbacks;
for (var i = 0, l = callbacks.length; i < l; i++) {
var cb = callbacks[i];
asset.addRef && asset.addRef();
cb.item.content = asset;
cb.done(err);
}
callbacks.length = 0;
}
});
pipeline.async(subTask);
}
依赖解析
getDepends (uuid, data, exclude, depends, preload, asyncLoadAssets, config) {
var err = null;
try {
var info = dependUtil.parse(uuid, data);
var includeNative = true;
if (data instanceof cc.Asset && (!data.__nativeDepend__ || data._nativeAsset)) includeNative = false;
if (!preload) {
asyncLoadAssets = !CC_EDITOR && (!!data.asyncLoadAssets || (asyncLoadAssets && !info.preventDeferredLoadDependents));
for (let i = 0, l = info.deps.length; i < l; i++) {
let dep = info.deps[i];
if (!(dep in exclude)) {
exclude[dep] = true;
depends.push({uuid: dep, __asyncLoadAssets__: asyncLoadAssets, bundle: config && config.name});
}
}
if (includeNative && !asyncLoadAssets && !info.preventPreloadNativeObject && info.nativeDep) {
config && (info.nativeDep.bundle = config.name);
depends.push(info.nativeDep);
}
} else {
for (let i = 0, l = info.deps.length; i < l; i++) {
let dep = info.deps[i];
if (!(dep in exclude)) {
exclude[dep] = true;
depends.push({uuid: dep, bundle: config && config.name});
}
}
if (includeNative && info.nativeDep) {
config && (info.nativeDep.bundle = config.name);
depends.push(info.nativeDep);
}
}
}
catch (e) {
err = e;
}
return err;
},
dependUtil是一个控制依赖列表的单例,通过传入uuid和asset对象来解析该对象的依赖资源列表,返回的依赖资源列表可能包含以下4个字段:
- deps 依赖的Asset资源
- nativeDep 依赖的原生资源
- preventPreloadNativeObject 禁止预加载原生对象,这个值默认是false
- preventDeferredLoadDependents 禁止延迟加载依赖,默认为false,对于骨骼动画、TiledMap等资源为true
- parsedFromExistAsset 是否直接从
asset.__depends__
中取出
dependUtil还维护了_depends缓存来避免依赖的重复查询,这个缓存会在首次查询某资源依赖时添加,当该资源被释放时移除
// 根据json信息获取其资源依赖列表,实际上json信息就是asset对象
parse (uuid, json) {
var out = null;
// 如果是场景或者Prefab,data会是一个数组,scene or prefab
if (Array.isArray(json)) {
// 如果已经解析过了,在_depends中有依赖列表,则直接返回
if (this._depends.has(uuid)) return this._depends.get(uuid)
out = {
// 对于Prefab或场景,直接使用_parseDepsFromJson方法返回
deps: cc.Asset._parseDepsFromJson(json),
asyncLoadAssets: json[0].asyncLoadAssets
};
}
// 如果包含__type__,获取其构造函数,并从json中查找依赖资源 get deps from json
// 实际测试,预加载的资源会走下面这个分支,预加载的资源并没有把json反序列化成Asset对象
else if (json.__type__) {
if (this._depends.has(uuid)) return this._depends.get(uuid);
var ctor = js._getClassById(json.__type__);
// 部分资源重写了_parseDepsFromJson和_parseNativeDepFromJson方法
// 比如cc.Texture2D
out = {
preventPreloadNativeObject: ctor.preventPreloadNativeObject,
preventDeferredLoadDependents: ctor.preventDeferredLoadDependents,
deps: ctor._parseDepsFromJson(json),
nativeDep: ctor._parseNativeDepFromJson(json)
};
out.nativeDep && (out.nativeDep.uuid = uuid);
}
// get deps from an existing asset
// 如果没有__type__字段,则无法找到它对应的ctor,从asset的__depends__字段中取出依赖
else {
if (!CC_EDITOR && (out = this._depends.get(uuid)) && out.parsedFromExistAsset) return out;
var asset = json;
out = {
deps: [],
parsedFromExistAsset: true,
preventPreloadNativeObject: asset.constructor.preventPreloadNativeObject,
preventDeferredLoadDependents: asset.constructor.preventDeferredLoadDependents
};
let deps = asset.__depends__;
for (var i = 0, l = deps.length; i < l; i++) {
var dep = deps[i].uuid;
out.deps.push(dep);
}
if (asset.__nativeDepend__) {
// asset._nativeDep会返回类似这样的对象 {__isNative__: true, uuid: this._uuid, ext: this._native}
out.nativeDep = asset._nativeDep;
}
}
// 第一次找到依赖,直接放到_depends列表中,cache dependency list
this._depends.add(uuid, out);
return out;
}
CCAsset默认的_parseDepsFromJson
和_parseNativeDepFromJson
实现如下,_parseDepsFromJson
通过调用parseDependRecursively递归json,将json对象及其子对象的所有__uuid__
全部找到放到depends数组中。Texture2D、TTFFont、AudioClip的实现为直接返回空数组,而SpriteFrame的实现为返回cc.assetManager.utils.decodeUuid(json.content.texture)
,这个字段记录了SpriteFrame对应纹理的uuid。
而_parseNativeDepFromJson
在改asset的_native
有值的情况下,会返回{ __isNative__: true, ext: json._native}
。实际上大部分的native资源走的是_nativeDep
,这个属性的get方法会返回一个包含类似这样的对象{__isNative__: true, uuid: this._uuid, ext: this._native}
。
_parseDepsFromJson (json) {
var depends = [];
parseDependRecursively(json, depends);
return depends;
},
_parseNativeDepFromJson (json) {
if (json._native) return { __isNative__: true, ext: json._native};
return null;
}