cocoscreator2.4资源加载流程

creator2.4.x 使用cc.AssetsManager管理资源,使用Asset Bundle 作为资源模块化工具。
默认bundle有internal、main,分别是引擎内置bundle,以及用户默认bundle;
如果主动创建了assets/resources文件夹,还会多一个resources bundle。

资源加载分为静态加载和动态加载
直接被编辑器节点树引用的资源,实例化时,引擎会自动静态加载;
需要动态加载的资源,一般放在resources目录下,使用cc.resources.loadAny加载;
但是家在资源本质上都是通过cc.AssetsManager的pipeline加载管线进行加载的

以下是一个已经配置为bundle的文件夹,以及其构建目录
image
image

import文件夹里都是json文件,存放cc.Asset资源的序列化信息,bundle所有的资源如cc.SceneAsset、cc.SpriteFrame、cc.Texture2D、cc.TextAsset等都会存在这里。Text或Json这类文本资源直接就内容填在这里了,非文本资源存则会声明依赖的是的哪些文件。
native文件夹是真正的资源文件目录,存放所有非文本资源,如图片、字体等文件。
config.json文件是上面所有这些资源的关联配置描述,存放资源路径和uuid的对应关系。
以下是uuid解码后的config.json:

调用loadBundle实际上就是缓存这个config配置,并不会缓存相应的资源,用到资源前,必须调用bundle.load开启加载流程。

cc.assetManager.loadBundle("test_bundle",(err,bundle:cc.AssetManager.Bundle)=>{
    if(err){
        return;
    }
    bundle.load("xxx/aa",cc.SpriteFrame,(err,frame:cc.SpriteFrame)=>{
        if(err){
            return;
        }
        cc.log("---------frame加载成功")
    });
})

我们今天主要研究的就是this.test_bundle.load("xxx/aa",cc.SpriteFrame)的内容了。
######简单描述一下加载流程
1. parse,根据path找到uuid,“xxx/aa”=>“4ea351e4-bb94-40e1-9aca-d7fd555ba770”
2. combine,根据uuid找到import下的配置文件路径 “assets/test_bundle/import/4e/4ea351e4-bb94-40e1-9aca-d7fd555ba770.json”


3. downloader下载该json配置文件,parse解析、反序列化该文件,生成一个cc.SpriteFrame对象,但是该对象暂无texture资源。此时本次加载管线执行完毕。
4. 再看看有没有要加载的依赖,有的话开启新的加载管线进行加载。此次依赖一个texture资源,uuid=“fd96705f-ace9-45ca-8f15-58f0cf33f12b”

也是先加载import下的json配置,然后创建cc.Texture2D对象,它依赖的就是native下的同名文件:
“assets/test_bundle/native/fd/fd96705f-ace9-45ca-8f15-58f0cf33f12b.png”
5. texture资源也加载完成以后,通过setProperties设置关联,将该texture赋值给第三步生成的cc.SpriteFrame
6. 加载完成,回调

######画了个加载管线的简易流程图

####下面,我们跟着代码一步步走
bundle.load调用cc.AssetManager.loadAny,传入初始化的options参数

load (paths, type, onProgress, onComplete) {
    var { type, onProgress, onComplete } = parseLoadResArgs(type, onProgress, onComplete);
    cc.assetManager.loadAny(paths, { __requestType__: RequestType.PATH, type: type, bundle: this.name, __outputAsArray__: Array.isArray(paths) }, onProgress, onComplete);
},

loadAny对参数进行校验,以适配不同的重载,然后创建Task,开启一个管线加载任务

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是在AssetManager初始化的:

    //this.pipeline管线分为preprocess和load两个管道
    this.pipeline = pipeline.append(preprocess).append(load);

pipeline.async即按管道顺序异步执行,先调用preprocess进行预处理,然后拿预处理后的结果再调用load。

preprocess函数在core/asset-manager/preprocess.js下:

function preprocess (task, done) {
    var options = task.options, subOptions = Object.create(null), leftOptions = Object.create(null);
    ///省略若干行,主要是剪裁options
    // transform url
    let subTask = Task.create({input: task.input, options: subOptions});
    var err = null;
    try {
        task.output = task.source = transformPipeline.sync(subTask);
    }
    catch (e) {
        err = e;
        for (var i = 0, l = subTask.output.length; i < l; i++) {
            subTask.output[i].recycle();
        }
    }
    subTask.recycle();
    done(err);
}

先对options参数进行剪裁,创建一个新的Task,再调用transformPipeline管线真正进行预处理,
transformPipeline也是再AssetManager里初始化的,分为parse和combine两个管道,
这两步没有异步操作,所以是同步执行两个管道,transformPipeline.sync

this.transformPipeline = transformPipeline.append(parse).append(combine);

parse和combine函数都在core/assets-manager/urlTransformer.js下,顾名思义,该文件的主要作用就是进行url转换,根据输入的资源相对路径,转换为实际的资源url。
parse函数主要逻辑:

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;

先找到对应的bundle配置,再根据目标文件名、Asset类型查找对应的资源info,然后保存资源uuid。
这一操作引擎也有导出,如 var info = bundle.getInfoWithPath('image/a', cc.Texture2D);
本次调用后,查到uuid=“4ea351e4-bb94-40e1-9aca-d7fd555ba770”

然后是combine

// 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}`;
}

其实就是用资源的uuid拼凑出资源路径
本次调用后,
base=“assets/test_bundle/import”,
uuid=“4ea351e4-bb94-40e1-9aca-d7fd555ba770”,
ver="",item.ext = “.json”
所以url = “assets/test_bundle/import/4e/4ea351e4-bb94-40e1-9aca-d7fd555ba770.json”
preprocess流程这就走完了。

然后是load管道,就是对得到的资源url进行下载、解析,主要用到loadOneAssetPipeline

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();

        packManager.load(item, task.options, function (err, data) {
            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) {
            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 {
            var { uuid } = item;
            if (uuid in exclude) {
                ///省略若干
            }
            else {
                if (!options.reload && assets.has(uuid)) {
                    ///省略若干
                }
                else {
                    parser.parse(id, file, 'import', options, function (err, asset) {
                        if (err) return done(err);
                        asset._uuid = uuid;
                        loadDepends(task, asset, done, true);
                    });
                }
            }
        }
    }
]);

第一步fetch,调用packManager.load,间接调用downloader.download,根据文件的后缀名来调用对应的下载方法,默认下载方法映射如下(core/asset-manager/downloader.js):

// dafault handler map
var downloaders = {
    // Images
    '.png' : downloadImage,
    '.jpg' : downloadImage,
    '.bmp' : downloadImage,
    '.jpeg' : downloadImage,
    '.gif' : downloadImage,
    '.ico' : downloadImage,
    '.tiff' : downloadImage,
    '.webp' : downloadImage,
    '.image' : downloadImage,
    '.pvr': downloadArrayBuffer,
    '.pkm': downloadArrayBuffer,

    // Audio
    '.mp3' : downloadAudio,
    '.ogg' : downloadAudio,
    '.wav' : downloadAudio,
    '.m4a' : downloadAudio,

    // Txt
    '.txt' : downloadText,
    '.xml' : downloadText,
    '.vsh' : downloadText,
    '.fsh' : downloadText,
    '.atlas' : downloadText,

    '.tmx' : downloadText,
    '.tsx' : downloadText,

    '.json' : downloadJson,
    '.ExportJson' : downloadJson,
    '.plist' : downloadText,

    '.fnt' : downloadText,

    // font
    '.font' : loadFont,
    '.eot' : loadFont,
    '.ttf' : loadFont,
    '.woff' : loadFont,
    '.svg' : loadFont,
    '.ttc' : loadFont,

    // Video
    '.mp4': downloadVideo,
    '.avi': downloadVideo,
    '.mov': downloadVideo,
    '.mpg': downloadVideo,
    '.mpeg': downloadVideo,
    '.rm': downloadVideo,
    '.rmvb': downloadVideo,

    // Binary
    '.binary' : downloadArrayBuffer,
    '.bin': downloadArrayBuffer,
    '.dbbin': downloadArrayBuffer,
    '.skel': downloadArrayBuffer,

    '.js': downloadScript,

    'bundle': downloadBundle,

    'default': downloadText

};

我们也可以自定义下载功能,如: cc.assetManager.downloader.register(".png",func)

本次调用downloadJson方法,回调一个json对象:

var downloadJson = function (url, options, onComplete) {
    options.responseType = "json";
    downloadFile(url, options, options.onFileProgress, function (err, data) {
        if (!err && typeof data === 'string') {
            try {
                data = JSON.parse(data);
            }
            catch (e) {
                err = e;
            }
        }
        onComplete && onComplete(err, data);
    });
};

然后存储该json对象,

packManager.load(item, task.options, function (err, data) {
    item.file = data;
    done(err);
});

这就下载完了,然后调用parse.parse进行解析,也是根据后缀名查找对应的解析方法:

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
};

同样,也可以自定义解析功能,如: cc.assetManager.parsers.register(".png",func)

这里用的是parseImport:

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);
}

其实就是反序列化,这里就是生成一个cc.SpriteFrame对象。

parser.parse(id, file, 'import', options, function (err, asset) {
    if (err) return done(err);
    asset._uuid = uuid;
    loadDepends(task, asset, done, true);
});

到这里本次管线就到尾声了,我们需要的cc.SpriteFrame对象也创建了,但是是一个空的对象,没有绑定texture纹理,而loadDepends就是干这个的。
就是获取到所有依赖资源,开启新的加载管线,加载完成以后调用setProperties进行绑定。
var missingAsset = setProperties(uuid, asset, map);
image
如上图调用关系,最终会绑定依赖的纹理,至此加载真正完成,完成回调。

    bundle.load("xxx/aa",cc.SpriteFrame,(err,frame:cc.SpriteFrame)=>{
        if(err){
            return;
        }
        cc.log("---------frame加载成功")
    });
15赞

感谢分享!

下一篇介绍了资源加密解密方案

mark k

厉害,写的比较清晰

mar k

如果对json进行加解密放parser是不是不行,downloader解析失败了

mark~~

mark!!!