Cocos Creator 新资源管理系统剖析【三:资源解析】

【本文参与征文活动】

在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;
        }
4赞

沙发 12345

深度好文:+1::tulip:

宝爷这是那个版本的

感谢楼主的分享,正好想了解资源加载方面的东西,mark 一下 ~~~

2.4.0,不过和后面的几个小版本差别不大

MARK.实力点赞!

mark,大佬牛逼