Analysis of CocosCreator's new resource management system

Analysis of CocosCreator's new resource management system

1. Resources and Construction

1.1 creator resource file basics

Before understanding how the engine parses and loads resources, let's first understand the rules of these resource files (pictures, prefabs, animations, etc.). There are several resource-related directories under the creator project directory:

  • assets The total directory of all resources, corresponding to the resource manager of the creator editor
  • library local resource library, the directory used when previewing the project
  • build The default directory of the project after building

In the assets directory, creator will generate a .meta file with the same name for each resource file and directory. The meta file is a json file that records the resource version, uuid, and various custom information (set in the editor's屬性檢查器). For example, the prefab meta file records properties such as optimizationPolicy and asyncLoadAssets that we can modify in the editor.

{
  "ver": "1.2.7",
  "uuid": "a8accd2e-6622-4c31-8a1e-4db5f2b568b5",
  "optimizationPolicy": "AUTO", // prefab creation optimization strategy "asyncLoadAssets": false, // Whether to delay loading "readonly": false,
  "subMetas": {}
}

In the imports directory under the library directory, the resource file name will be converted into uuid, and the first two characters of the uuid will be taken to group the directory for storage. The creator will put the mapping relationship between the uuid of all resources and the assets directory, as well as the last updated timestamp of the resources and meta into a file named uuid-to-mtime.json, as shown below.

{
  "9836134e-b892-4283-b6b2-78b5acf3ed45": {
    "asset": 1594351233259,
    "meta": 1594351616611,
    "relativePath": "effects"
  },
  "430eccbf-bf2c-4e6e-8c0c-884bbb487f32": {
    "asset": 1594351233254,
    "meta": 1594351616643,
    "relativePath": "effects\\__builtin-editor-gizmo-line.effect"
  },
  ...
}

Compared with the resources in the assets directory, the resources in the library directory merge the information of the meta file. The file directory is only recorded in uuid-to-mtime.json, and the library directory does not generate anything for the directory.

1.2 Resource Construction

After the project is built, the resources will be moved from the library directory to the build directory of the build output. Basically, only the scenes involved in the build and the resources in the resources directory, as well as the resources they reference, will be exported. Script resources will be merged from multiple js scripts into one js, and various json files will also be packaged according to specific rules. We can set up bundles and projects in the bundle configuration interface and project build interface

1.2.1 Pictures, atlases, and automatic atlases

  • https://docs.cocos.com/creator/manual/zh/asset-workflow/sprite.html
  • https://docs.cocos.com/creator/manual/zh/asset-workflow/atlas.html
  • https://docs.cocos.com/creator/manual/zh/asset-workflow/auto-atlas.html

A json file will be generated for each image imported into the editor to describe the Texture information, as shown below. By default, all Texture2D json files in the project will be compressed into one. If無壓縮is selected, a Texture2D json file will be generated for each image.

{
  "__type__": "cc.Texture2D",
  "content": "0,9729,9729,33071,33071,0,0,1"
}

If you set the texture's Type property to Sprite, Creator will also automatically generate a json file of the SpriteFrame type.
In addition to the image, the atlas resource also corresponds to an atlas json, which contains the cc.SpriteAtlas information and the SpriteFrame information of each fragment. The automatic atlas only contains the cc.SpriteAtlas information by default. When all SpriteFrames are inlined, all SpriteFrames will be merged.

1.2.2 Prefab and Scene

  • https://docs.cocos.com/creator/manual/zh/asset-workflow/prefab.html
  • https://docs.cocos.com/creator/manual/en/asset-workflow/scene-managing.html

Scene resources are very similar to Prefab resources. They are both json files that describe all nodes, components, and other information. When內聯所有SpriteFrame is checked, the SpriteFrames referenced by the Prefab will be merged into the json file where the prefab is located. If a SpriteFrame is referenced by multiple prefabs, the json file of each prefab will contain the information of the SpriteFrame. If you do not check內聯所有SpriteFrame , the SpriteFrame will be a separate json file.

1.2.3 Resource file merging rules

When Creator merges multiple resources into one json file, we can find the打包resource information in the packs field in config.json. A resource may be repeatedly packaged into multiple json files. Here is an example showing the construction rules of creator under different options:

  • a.png A single Sprite type image
  • dir/b.png, c.png, AutoAtlas The dir directory contains 2 images and an AutoAtlas
  • d.png, d.plist normal atlas
  • e.prefab references the prefab of SpriteFrame a and b
  • f.prefab references the prefab of SpriteFrame b

The following are the files built according to different rules. You can see that the number of files generated without compression is the largest. Non-inline files will be more than inline files, but inline may cause the same file to be included repeatedly. For example, both Prefabs e and f reference the same image, and the SpriteFrame.json of this image will be included repeatedly. If they are merged into one json, only one file will be generated.

Resource Files No compression Default (not inline) Default (inline) Merge json
a.png a.texture.json + a.spriteframe.json a.spriteframe.json
./dir/b.png b.texture.json + b.spriteframe.json b.spriteframe.json
./dir/c.png c.texture.json + c.spriteframe.json c.spriteframe.json c.spriteframe.json
./dir/AutoAtlas autoatlas.json autoatlas.json autoatlas.json
d.png d.texture.json + d.spriteframe.json d.spriteframe.json d.spriteframe.json
d.plist d.plist.json d.plist.json d.plist.json
e.prefab e.prefab.json e.prefab.json e.prefab.json(pack a+b)
f.prefab f.prefab.json f.prefab.json f.prefab.json(pack b)
g.allTexture.json g.allTexture.json all.json

The default option is a good choice in most cases. If it is a web platform, it is recommended to check內聯所有SpriteFrame , which can reduce network io and improve performance. It is not recommended to check it for native platforms, which may increase the package size and the content to be downloaded during hot updates. For some compact bundles (for example, loading the bundle requires all the resources in it), we can configure it to merge all the json.

2. Understanding and using Asset Bundle

2.1 Create a Bundle

Asset Bundle is a resource management solution after Creator 2.4. Simply put, it plans resources through directories, puts various resources into different directories according to project requirements, and configures the directories into Asset Bundles. It can play the following roles:

  • Speed ​​up game launch time
  • Reduce the size of the first package
  • Reuse resources across projects
  • Convenient implementation of sub-games
  • Hot update in bundle units

Creating an Asset Bundle is very simple. Just check the box in the directory's屬性檢查器to配置為bundle . The official documentation has a detailed introduction to the options.

The document does not provide a detailed description of compression. The compression here does not refer to compression such as zip, but to merging multiple resource json files into one through packAssets to reduce io.

It is very simple to check the options. The real key lies in how to plan the Bundle. The planning principles are to reduce the package size, speed up startup and reuse resources. It is a good choice to plan resources according to the modules of the game, such as by sub-games, level copies, or system functions.

Bundle will automatically package the resources in the folder, as well as the resources in other folders referenced by the folder (if these resources are not in other bundles). If we plan resources according to modules, it is easy for multiple bundles to share a resource. You can extract common resources into a bundle, or set a bundle to have a higher priority and build bundle dependencies. Otherwise, these resources will be placed in multiple bundles at the same time (if it is a local bundle, this will cause the package size to increase).

2.2 Using Bundle

  • About loading resources https://docs.cocos.com/creator/manual/zh/scripting/load-assets.html
  • About releasing resources https://docs.cocos.com/creator/manual/zh/asset-manager/release-manager.html

The use of Bundle is also very simple. If it is a resource in the resources directory, you can directly use cc.resources.load to load it.

cc.resources.load("test assets/prefab", function (err, prefab) {
    var newNode = cc.instantiate(prefab);
    cc.director.getScene().addChild(newNode);
});

If it is other custom Bundles (local Bundles or remote Bundles can be loaded using the Bundle name), you can use cc.assetManager.loadBundle to load the Bundle, and then use the loaded Bundle object to load the resources in the Bundle. For native platforms, if the Bundle is configured as a remote package, you need to fill in the resource server address in the build release panel during building.

cc.assetManager.loadBundle('01_graphics', (err, bundle) => {
    bundle.load('xxx');
});

On native or mini-game platforms, we can also use Bundle like this:

  • If you want to load a remote Bundle from another project, you need to use the URL to load it (other project refers to another cocos project)
  • If you want to manage the download and cache of the bundle yourself, you can put it in a local writable path and pass in the path to load these bundles.
// When reusing the Asset Bundle of other projects cc.assetManager.loadBundle('https://othergame.com/remote/01_graphics', (err, bundle) => {
    bundle.load('xxx');
});

// Native platform cc.assetManager.loadBundle(jsb.fileUtils.getWritablePath() + '/pathToBundle/bundleName', (err, bundle) => {
    // ...
});

// WeChat Mini Game Platform cc.assetManager.loadBundle(wx.env.USER_DATA_PATH + '/pathToBundle/bundleName', (err, bundle) => {
    // ...
});

Other notes:

  • Loading a Bundle only loads the Bundle's configuration and scripts. Other resources in the Bundle also need to be loaded separately.
  • Currently, native Bundle does not support zip packaging. The remote package download method is to download files one by one. The advantage is simple operation and convenient update. The disadvantage is that there are many IOs and large traffic consumption.
  • Do not use the same name for script files in different bundles
  • A bundle A depends on another bundle B. If B is not loaded, loading A will not automatically load B. Instead, an error will be reported when loading the resource that A depends on B.

3. Analysis of the new resource framework

The new framework code after v2.4 refactoring is more concise and clear. We can first understand the entire resource framework from a macro perspective. The resource pipeline is the core part of the entire framework. It standardizes the entire resource loading process and supports customization of the pipeline.

Public Documents

  • helper.js defines a bunch of common functions, such as decodeUuid, getUuidFromURL, getUrlWithUuid, etc.
  • utilities.js defines a bunch of common functions, such as getDepends, forEach, parseLoadResArgs, etc.
  • deserialize.js defines the deserialize method, which deserializes the json object into an Asset object and sets its __depends__ property.
  • depend-util.js controls the dependency list of resources. All dependencies of each resource are placed in the _depends member variable
  • cache.js is a general cache class that encapsulates a simple key-value pair container
  • shared.js defines some global objects, mainly Cache and Pipeline objects, such as loaded assets, downloaded files and bundles, etc.

Bundle

  • config.js bundle configuration object, responsible for parsing the bundle config file
  • bundle.js bundle class, encapsulates config and related interfaces for loading and unloading resources in the bundle
  • builtins.js is a package of built-in bundle resources, which can be accessed through cc.assetManager.builtins

Pipeline part

CCAssetManager.js manages the pipeline and provides a unified loading and unloading interface

Pipeline framework

  • pipeline.js implements basic functions such as pipeline combination and flow
  • task.js defines the basic properties of a task and provides a simple task pool function
  • request-item.js defines the basic properties of a resource download item. A task may generate multiple download items.

Pretreatment pipeline

  • urlTransformer.js parse converts request parameters into RequestItem objects (and queries related resource configurations), and combine is responsible for converting the actual URL
  • preprocess.js filters out resources that need to be converted to URLs and calls transformPipeline

Download pipeline

  • download-dom-audio.js provides a method to download audio effects, using the audio tag to download
  • download-dom-image.js provides a method to download images using the Image tag
  • download-file.js provides a method to download files using XMLHttpRequest
  • download-script.js provides a method to download scripts, using the script tag to download
  • downloader.js supports downloading all formats of files, concurrent control, and retry upon failure.

Parsing pipeline

  • factory.js creates factories for Bundle, Asset, Texture2D and other objects
  • fetch.js calls packManager to download resources and resolve dependencies
  • parser.js parses the downloaded files

other

  • releaseManager.js provides a resource release interface, responsible for releasing dependent resources and releasing resources when switching scenes
  • cache-manager.d.ts is used to manage all caches downloaded from the server on non-WEB platforms.
  • pack-manager.js handles packaged resources, including unpacking, loading, caching, etc.

3.1 Loading pipeline

Creator uses pipelines to handle the entire resource loading process. The advantage of this is that it decouples the resource processing process and separates each step into a separate pipeline. The pipeline can be easily reused and combined, and it is convenient for us to customize the entire loading process. We can create some of our own pipelines and add them to the pipeline, such as resource encryption.

AssetManager has three built-in pipelines: the normal loading pipeline, the preloading pipeline, and the resource path conversion pipeline. The last pipeline serves the first two pipelines.

// Normal loading this.pipeline = pipeline.append(preprocess).append(load);
// Preload this.fetchPipeline = fetchPipeline.append(preprocess).append(fetch);
// Convert resource path this.transformPipeline = transformPipeline.append(parse).append(combine);

3.1.1 Start the loading pipeline [Loading interface]

Next, let's take a look at how a common resource is loaded, such as the simplest cc.resource.load. In the bundle.load method, cc.assetManager.loadAny is called. In the loadAny method, a new task is created and the async method of the normal loading pipeline is called to execute the task.

Note that the resource path to be loaded is placed in task.input, and options is an object that contains fields such as type, bundle, and __requestType__

// The load method of the bundle class 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 }, onProgress, onComplete);
},

// assetManager's loadAny method loadAny (requests, options, onProgress, onComplete) {
  var { options, onProgress, onComplete } = parseParameters(options, onProgress, onComplete);
  
  options.preset = options.preset || 'default';
  let task = new Task({input: requests, onProgress, onComplete: asyncify(onComplete), options});
  pipeline.async(task);
},

The pipeline consists of two parts: preprocess and load. preprocess consists of the following pipelines: preprocess, transformPipeline { parse, combine }. Preprocess actually only creates a subtask, which is then executed by transformPipeline. For loading a normal resource, the subtask's input and options are the same as the parent task.

let subTask = Task.create({input: task.input, options: subOptions});
task.output = task.source = transformPipeline.sync(subTask);

3.1.2 transformPipeline pipeline [preparation phase]

transformPipeline consists of two pipelines, parse and combine. The responsibility of parse is to generate a RequestItem object for each resource to be loaded and initialize its resource information (AssetInfo, uuid, config, etc.):

First convert the input into an array for traversal. If you are loading resources in batches, each add-in will generate a RequestItem

If the input item is an object, copy the options to the item first (in fact, every item will be an object. If it is a string, it will be converted to an object in the first step)

  • For UUID type items, first check the bundle and extract the AssetInfo from the bundle. For redirect type resources, get the AssetInfo from the dependent bundle. If the bundle cannot be found, an error is reported.
  • The processing of PATH type, SCENE type and UUID type is basically similar, all of which are to obtain detailed information of the resource.
  • The DIR type will extract the information of the specified path from the bundle and append it to the end of the input in batches (generating additional add-ons).
  • The URL type is a remote resource type and does not require special processing
function parse (task) {
    //Convert input into an array 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') {
            // Create object first
            item = Object.create(null);
            item[options.__requestType__ || RequestType.UUID] = input[i];
        }
        if (typeof item === 'object') {
            // local options will overlap glabal options
            //Copy the properties of options to item. Addon will copy the properties that are in options but not in item. cc.js.addon(item, options);
            if (item.preset) {
                cc.js.addon(item, cc.assetManager.presets[item.preset]);
            }
            for (var key in item) {
                switch (key) {
                    // uuid type resource, get the detailed information of the resource from the bundle case RequestType.UUID: 
                        var uuid = out.uuid = decodeUuid(item.uuid);
                        if (bundles.has(item.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: 
                        // After unpacking, dynamically add it to the end of the input list. Subsequent loops will automatically parse these resources if (bundles.has(item.bundle)) {
                            var infos = [];
                            bundles.get(item.bundle)._config.getDirWithPath(item.dir, item.type, infos);
                            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});
                            }
                        }
                        out.recycle();
                        out = null;
                        break;
                    case RequestType.PATH: 
                        // A resource of type PATH retrieves detailed information about the resource based on the path and type 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:
                        // Scene type, call getSceneInfo from the config in the bundle to get detailed information about the 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: 
                        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;
        task.output.push(out);
        if (!out.uuid && !out.url) throw new Error('unknown input:' + item.toString());
    }
    return null;
}

The initial information of RequestItem is queried from the bundle object, and the bundle information is initialized from the config.json file that comes with the bundle. When the bundle is packaged, the resource information in the bundle will be written to config.json.

After being processed by the parse method, we will get a series of RequestItems, and many RequestItems come with information such as AssetInfo and uuid. The combine method will build a real loading path for each RequestItem, and this loading path will eventually be converted to item.url.

function combine (task) {
    var input = task.output = task.input;
    for (var i = 0; i < input.length; i++) {
        var item = input[i];
        // If the item already contains a URL, skip this and use the item's URL directly
        if (item.url) continue;

        var url = '', base = '';
        var config = item.config;
        // Determine the directory prefix if (item.isNative) {
            base = (config && config.nativeBase) ? (config.base + config.nativeBase) : cc.assetManager.generalNativeBase;
        } 
        else {
            base = (config && config.importBase) ? (config.base + config.importBase) : cc.assetManager.generalImportBase;
        }

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

        // Concatenate the final url
        // 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}`;
        }
        
        item.url = url;
    }
    return null;
}

3.1.3 Load pipeline [loading process]

The load method is very simple. It basically just creates a new task and executes each subtask in loadOneAssetPipeline.

function load (task, done) {
    if (!task.progress) {
        task.progress = {finish: 0, total: task.input.length};
    }
    
    var options = task.options, progress = task.progress;
    options.__exclude__ = options.__exclude__ || Object.create(null);
    task.output = [];
    forEach(task.input, function (item, cb) {
        // Create a subtask for each input item and assign it to loadOneAssetPipeline for execution let subTask = Task.create({ 
            input: item, 
            onProgress: task.onProgress, 
            options, 
            progress, 
            onComplete: function (err, item) {
                if (err && !task.isFinish && !cc.assetManager.force) done(err);
                task.output.push(item);
                subTask.recycle();
                cb();
            }
        });
        // Execute subtasks. loadOneAssetPipeline consists of fetch and parse. loadOneAssetPipeline.async(subTask);
    }, function () {
        // After each input is executed, the function is executed last options.__exclude__ = null;
        if (task.isFinish) {
            clear(task, true);
            return task.dispatch('error');
        }
        gatherAsset(task);
        clear(task, true);
        done();
    });
}

As its function name suggests, loadOneAssetPipeline is a pipeline for loading an asset. It is divided into two steps, fetch and parse:

The fetch method is used to download resource files. PackManager is responsible for the download implementation. Fetch will put the downloaded file data into item.file

The parse method is used to convert the loaded resource file into a resource object that we can use.

For native resources, call parser.parse for parsing. This method will call different parsing methods according to the resource type.

  • Import resources call the parseImport method, deserialize the Asset object according to the JSON data, and put it in assets
  • The image resource will call the parseImage, parsePVRTex, or parsePKMTex method to parse the image format (but will not create a Texture object).
  • The sound effect resource calls the parseAudio method for parsing
  • The plist resource calls the parsePlist method to parse

For other resources

If uuid is in task.options.__exclude__ , it is marked as completed and the reference count is added. Otherwise, some complex conditions are used to determine whether to load the resource dependency.

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 the resource has been loaded in assets, complete it directly if (file || (!reload && !isNative && assets.has(uuid))) return done();
        // Download the file. This is an asynchronous process. After the file is downloaded, it will be placed in item.file and the done driver pipeline will be executed packManager.load(item, task.options, function (err, data) {
            if (err) {
                if (cc.assetManager.force) {
                    err = null;
                } else {
                    cc.error(err.message, err.stack);
                }
                data = null;
            }
            item.file = data;
            done(err);
        });
    },
    // The process of converting resource files into resource objects 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) {
            // For native resources, call parser.parse to process them, put the processed resources into item.content, and end the process parser.parse(id, file, item.ext, options, function (err, asset) {
                if (err) {
                    if (!cc.assetManager.force) {
                        cc.error(err.message, err.stack);
                        return done(err);
                    }
                }
                item.content = asset;
                task.dispatch('progress', ++progress.finish, progress.total, item);
                files.remove(id);
                parsed.remove(id);
                done();
            });
        } else {
            var { uuid } = item;
            // Non-native resources, if in task.options.__exclude__, end directly if (uuid in exclude) {
                var { finish, content, err, callbacks } = exclude[uuid];
                task.dispatch('progress', ++progress.finish, progress.total, item);
    
                if (finish || checkCircleReference(uuid, uuid, exclude) ) {
                    content && content.addRef();
                    item.content = content;
                    done(err);
                } else {
                    callbacks.push({ done, item });
                }
            } else {
                // If it is not reload, and the asset contains the uuid
                if (!options.reload && assets.has(uuid)) {
                    var asset = assets.get(uuid);
                    // If options.__asyncLoadAssets__ is enabled or asset.__asyncLoadAssets__ is false, the process ends without loading dependencies if (options.__asyncLoadAssets__ || !asset.__asyncLoadAssets__) {
                        item.content = asset.addRef();
                        task.dispatch('progress', ++progress.finish, progress.total, item);
                        done();
                    }
                    else {
                        loadDepends(task, asset, done, false);
                    }
                } else {
                    // If it is reload, or it is not in assets, parse it and load the dependencies.parse(id, file, 'import', options, function (err, asset) {
                        if (err) {
                            if (cc.assetManager.force) {
                                err = null;
                            }
                            else {
                                cc.error(err.message, err.stack);
                            }
                            return done(err);
                        }
                        
                        asset._uuid = uuid;
                        loadDepends(task, asset, done, true);
                    });
                }
            }
        }
    }
]);

3.2 File Download

Creator uses packManager.load to complete the download work. When downloading a file, there are two issues to consider:

  • Whether the file is packaged, for example, because all SpriteFrames are inlined, the SpriteFrame json file is merged into the prefab
  • The current platform is a native platform or a web platform. For some local resources, the native platform needs to read them from disk.
// Implementation of packManager.load load (item, options, onComplete) {
  // If the resource is not packaged, call downloader.download directly to download it (download also has judgments on whether it has been downloaded or is loading)
  if (item.isNative || !item.info || !item.info.packs) return downloader.download(item.id, item.url, item.ext, item.options, onComplete);
  // If the file has been downloaded, return directly if (files.has(item.id)) return onComplete(null, files.get(item.id));

  var packs = item.info.packs;
  // If pack is already loading, add the callback to the _loading queue and trigger the callback after loading is complete var pack = packs.find(isLoading);
  if (pack) return _loading.get(pack.uuid).push({ onComplete, id: item.id });

  // Download a new pack
  pack = packs[0];
  _loading.add(pack.uuid, [{ onComplete, id: item.id }]);
  let url = cc.assetManager._transform(pack.uuid, {ext: pack.ext, bundle: item.config.name});
  // Download the pack and unpack it,
  downloader.download(pack.uuid, url, pack.ext, item.options, function (err, data) {
      files.remove(pack.uuid);
      if (err) {
          cc.error(err.message, err.stack);
      }
      // unpack package, the internal implementation includes 2 kinds of unpacking, one for the segmentation and unpacking of json arrays such as prefab and atlas, and the other for the content of Texture2D packManager.unpack(pack.packs, data, pack.ext, item.options, function (err, result) {
          if (!err) {
              for (var id in result) {
                  files.add(id, result[id]);
              }
          }
          var callbacks = _loading.remove(pack.uuid);
          for (var i = 0, l = callbacks.length; i < l; i++) {
              var cb = callbacks[i];
              if (err) {
                  cb.onComplete(err);
                  continue;
              }

              var data = result[cb.id];
              if (!data) {
                  cb.onComplete(new Error('can not retrieve data from package'));
              }
              else {
                  cb.onComplete(null, data);
              }
          }
      });
  });
}

3.2.1 Downloading from the Web Platform

The download implementation of the web platform is as follows:

  • Use a downloaders array to manage the download methods corresponding to various resource types
  • Use files cache to avoid duplicate downloads
  • Use the _downloading queue to handle callbacks when downloading the same resource concurrently and ensure timing
  • Supports download priority, retry and other logic
download (id, url, type, options, onComplete) {
  // Get the corresponding type of download callback in downloaders let func = downloaders[type] || downloaders['default'];
  let self = this;
  // Avoid repeated downloads let file, downloadCallbacks;
  if (file = files.get(id)) {
      onComplete(null, file);
  }
  // If downloading, add to the queue 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 {
      // Download and set up retries for failed downloads var maxRetryCount = options.maxRetryCount || this.maxRetryCount;
      var maxConcurrency = options.maxConcurrency || this.maxConcurrency;
      var maxRequestsPerFrame = options.maxRequestsPerFrame || this.maxRequestsPerFrame;

      function process (index, callback) {
          if (index === 0) {
              _downloading.add(id, [onComplete]);
          }
          if (!self.limited) return func(urlAppendTimestamp(url), options, callback);
          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 requests reaches 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 is finished, add the file to the files cache, remove it from the _downloading queue, and execute callbacks. // when retry is finished, invoke callbacks
      function finale (err, result) {
          if (!err) files.add(id, result);
          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);
  }
}

Downloaders is a map that maps the download methods corresponding to various resource types. On the web platform, it mainly includes the following types of download methods:

Image class downloadImage

  • downloadDomImage Use the Image element of Html and specify its src attribute to download
  • downloadBlob Download the image as a file

File class, which can be divided into binary files, json files and text files

  • downloadArrayBuffer specifies the arraybuffer type to call downloadFile for downloading files such as skel, bin, pvr, etc.
  • downloadText specifies the text type to call downloadFile, which is used for downloading files such as atlas, tmx, xml, and vsh.
  • downloadJson specifies the json type to call downloadFile, and parses the json after downloading, which is used for downloading files such as plist and json

Font class loadFont builds css style and specifies url to download

Sound class downloadAudio

  • downloadDomAudio creates an Html audio element and specifies its src attribute to download
  • downloadBlob Download the sound effect as a file

The video class downloadVideo web client directly returns

The script downloadScript creates an Html script element and specifies its src attribute to download and execute

Bundle downloadBundle downloads the Bundle's json and scripts at the same time

downloadFile uses XMLHttpRequest to download files. The specific implementation is as follows:

function downloadFile (url, options, onProgress, onComplete) {
    var { options, onProgress, onComplete } = parseParameters(options, onProgress, onComplete);
    var xhr = new XMLHttpRequest(), errInfo = 'download failed: ' + url + ', status: ';
    xhr.open('GET', url, true);
    
    if (options.responseType !== undefined) xhr.responseType = options.responseType;
    if (options.withCredentials !== undefined) xhr.withCredentials = options.withCredentials;
    if (options.mimeType !== undefined && xhr.overrideMimeType ) xhr.overrideMimeType(options.mimeType);
    if (options.timeout !== undefined) xhr.timeout = options.timeout;

    if (options.header) {
        for (var header in options.header) {
            xhr.setRequestHeader(header, options.header[header]);
        }
    }

    xhr.onload = function () {
        if ( xhr.status === 200 || xhr.status === 0 ) {
            onComplete && onComplete(null, xhr.response);
        } else {
            onComplete && onComplete(new Error(errInfo + xhr.status + '(no response)'));
        }

    };

    if (onProgress) {
        xhr.onprogress = function (e) {
            if (e.lengthComputable) {
                onProgress(e.loaded, e.total);
            }
        };
    }

    xhr.onerror = function(){
        onComplete && onComplete(new Error(errInfo + xhr.status + '(error)'));
    };
    xhr.ontimeout = function(){
        onComplete && onComplete(new Error(errInfo + xhr.status + '(time out)'));
    };
    xhr.onabort = function(){
        onComplete && onComplete(new Error(errInfo + xhr.status + '(abort)'));
    };

    xhr.send(null);
    return xhr;
}

3.2.2 Native platform download

The engine-related files of the native platform can be found in resources/builtin/jsb-adapter/engine . The resource loading-related implementation is in the jsb-loader.js file, where the downloader re-registers the callback function.

downloader.register({
    // JS
    '.js' : downloadScript,
    '.jsc' : downloadScript,

    // Images
    '.png' : downloadAsset,
    '.jpg' : downloadAsset,
    ...
});

On native platforms, methods such as downloadAsset will call download to download resources. Before downloading resources, transformUrl will be called to detect the URL, mainly to determine whether the resource is a network resource or a local resource, and if it is a network resource, whether it has been downloaded. Only network resources that have not been downloaded need to be downloaded. Files that do not need to be downloaded will be read directly where the file is parsed.

// func passes in the processing after the download is complete. For example, after the script is downloaded, it needs to be executed, and window.require will be called at this time
// If you want to download a json resource, the passed func is doNothing, which means directly calling the onComplete method function download (url, func, options, onFileProgress, onComplete) {
    var result = transformUrl(url, options);
    // If it is a local file, point directly to func
    if (result.inLocal) {
        func(result.url, options, onComplete);
    }
    // If in the cache, update the last used time (lru) of the resource
    else if (result.inCache) {
        cacheManager.updateLastTime(url)
        func(result.url, options, function (err, data) {
            if (err) {
                cacheManager.removeCache(url);
            }
            onComplete(err, data);
        });
    }
    else {
        // For network resources that have not been downloaded, call downloadFile to download var time = Date.now();
        var storagePath = '';
        if (options.__cacheBundleRoot__) {
            storagePath = `${cacheManager.cacheDir}/${options.__cacheBundleRoot__}/${time}${suffix++}${cc.path.extname(url)}`;
        }
        else {
            storagePath = `${cacheManager.cacheDir}/${time}${suffix++}${cc.path.extname(url)}`;
        }
        // Download and cache using downloadFile downloadFile(url, storagePath, options.header, onFileProgress, function (err, path) {
            if (err) {
                onComplete(err, null);
                return;
            }
            func(path, options, function (err, data) {
                if (!err) {
                    cacheManager.cacheFile(url, storagePath, options.__cacheBundleRoot__);
                }
                onComplete(err, data);
            });
        });
    }
}

function transformUrl (url, options) {
    var inLocal = false;
    var inCache = false;
    // Check if it is a URL by regular expression
    if (REGEX.test(url)) {
        if (options.reload) {
            return { url };
        }
        else {
            // Check if it is in the cache (local disk cache)
            var cache = cacheManager.cachedFiles.get(url);
            if (cache) {
                inCache = true;
                url = cache.url;
            }
        }
    }
    else {
        inLocal = true;
    }
    return { url, inLocal, inCache };
}

downloadFile will call the native platform's jsb_downloader to download resources and save them to the local disk

downloadFile (remoteUrl, filePath, header, onProgress, onComplete) {
  downloading.add(remoteUrl, { onProgress, onComplete });
  var storagePath = filePath;
  if (!storagePath) storagePath = tempDir + '/' + performance.now() + cc.path.extname(remoteUrl);
  jsb_downloader.createDownloadFileTask(remoteUrl, storagePath, header);
},

3.3 File parsing

In loadOneAssetPipeline, resources are processed by two pipelines: fetch and parse. Fetch is responsible for downloading, while parse is responsible for parsing resources and instantiating resource objects. In the parse method, parser.parse is called to pass in the file content, parse it into the corresponding Asset object, and return it.

3.3.1 Web Platform Analysis

The main function of parser.parse on the Web platform is to manage the files being parsed, maintain a list of files being parsed and parsed, and avoid repeated parsing. At the same time, a list of callbacks after parsing is completed is maintained, and the actual parsing method is in the parsers array.

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 map the parsing methods of various types of files. The following takes pictures and ordinary asset resources as examples:

Note: In the parseImport method, the deserialization method will put the resource dependencies into asset.__depends__, which is an array. Each object in the array contains three fields: resource id uuid, owner object, and prop attribute. For example, a Prefab resource has two nodes, both referencing the same resource. The depends list needs to record a dependency information for each of these two node objects [{uuid:xxx, owner:1, prop:tex}, {uuid:xxx, owner:2, prop:tex}]

// Mapping image formats to parsing methods 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
};

// The image is not parsed into an Asset object, but into the corresponding image object 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 object parsing is implemented through deserialize. The general process is to parse json and find the corresponding class, call the _deserialize method of the corresponding class to copy data, initialize variables, and put dependent resources into 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);
},

3.3.2 Native platform analysis

On the native platform, jsb-loader.js re-registers the parsing methods of various resources:

parser.register({
    '.png' : downloader.downloadDomImage,
    '.binary' : parseArrayBuffer,
    '.txt' : parseText,
    '.plist' : parsePlist,
    '.font' : loadFont,
    '.ExportJson' : parseJson,
    ...
});

The image parsing method is actually downloader.downloadDomImage? After tracing the native platform and debugging, it is indeed this method that is called. An Image object is created and src is specified to load the image. This method can also load images from the local disk, but how is the texture object created? Through the json file corresponding to Texture2D, creator has already created the Texture2D Asset object before loading the real native texture. After loading the native image resource, the Image object will be set to the _nativeAsset of the Texture2D object. In the set method of this property, initWithData or initWithElement will be called, where the texture data is actually used to create the texture object for rendering.

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

As for the implementations of parseJson, parseText, parseArrayBuffer, etc., they simply call the file system to read the file. What about some resources that need further parsing before they can be used after obtaining the file content? For example, models, skeletons and other resources rely on binary model data. Where is the analysis of this data? That’s right, just like the Texture2D above, they are all placed in the corresponding Asset resource itself. Some are initialized in the setter callback of the _nativeAsset field, while some are initialized lazily when the resource is actually used.

// In jsb-loader.js file function parseText (url, options, onComplete) {
    readText(url, onComplete);
}

function parseArrayBuffer (url, options, onComplete) {
    readArrayBuffer(url, onComplete);
}

function parseJson (url, options, onComplete) {
    readJson(url, onComplete);
}

// In jsb-fs-utils.js file 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);
        });
    },

How are resources like atlases and Prefabs initialized? Creator still uses the parseImport method for parsing, because the type corresponding to these resources is import , and the native platform does not cover the parse function corresponding to this type, and these resources will be directly deserialized into usable Asset objects.

3.4 Dependency Loading

Creator divides resources into two categories, common resources and native resources. Common resources include cc.Asset and its subclasses, such as cc.SpriteFrame, cc.Texture2D, cc.Prefab, etc. Native resources include textures, music, fonts and other files in various formats. We cannot use these native resources directly in the game, but need to let the creator convert them into corresponding cc.Asset objects before we can use them.

In creator, a Prefab may depend on many resources. These dependencies can also be divided into common dependencies and native resource dependencies. Creator's cc.Asset provides _parseDepsFromJson and _parseNativeDepFromJson methods to check resource dependencies. loadDepends collects resource dependencies through the getDepends method.

loadDepends creates a subtask to load dependent resources and calls pipeline to perform the loading. In fact, this logic will be executed regardless of whether there are dependencies to be loaded. After the loading is completed, the following important logic will be executed:

  • Initialize assset: After the dependencies are loaded, assign the dependent resources to the corresponding properties of the asset and call asset.onLoad
  • Remove the files and parsed cache corresponding to the resource, and cache the resource into assets (if it is a scene, it will not be cached)
  • Execute the callbacks in the repeatItem.callbacks list (constructed at the beginning of loadDepends, recording the passed in done method by default)
// Load the dependencies of the specified 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 = [];
    // Increase the reference count to avoid resources being released during the process of loading dependencies, and call getDepends to obtain dependent resources 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) {
            // Callback after all dependencies are loaded 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];
                //Construct a map to record the mapping from uuid to 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);
                }

                // Call setProperties to set the corresponding dependent resources to the member variables of assetif (!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();
            }
            
            // This repeatItem may be loaded from many places, and all callbacks need to be notified of the loading completion 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);
}

3.4.1 Dependency Resolution

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 is a singleton that controls the dependency list. It parses the object's dependency resource list by passing in uuid and asset objects. The returned dependency resource list may contain the following four fields:

  • Deps dependent Asset resources
  • nativeDep native resources that depend on
  • preventPreloadNativeObject prohibits preloading of native objects. This value defaults to false
  • preventDeferredLoadDependents prevents delayed loading of dependencies. The default value is false. It is true for resources such as skeletal animation and TiledMap.
  • parsedFromExistAsset Whether to take it directly from asset.__depends__

dependUtil also maintains a _depends cache to avoid repeated queries of dependencies. This cache is added when a resource dependency is first queried and removed when the resource is released.

// Get the resource dependency list based on the json information. In fact, the json information is the asset object parse (uuid, json) {
  var out = null;
  // If it is a scene or Prefab, data will be an array, scene or prefab
  if (Array.isArray(json)) {
      // If it has been parsed and there is a dependency list in _depends, return directly if (this._depends.has(uuid)) return this._depends.get(uuid)
      out = {
          // For Prefab or scene, directly use the _parseDepsFromJson method to return deps: cc.Asset._parseDepsFromJson(json),
          asyncLoadAssets: json[0].asyncLoadAssets
      };
  }
  // If __type__ is included, get its constructor and find dependent resources from json get deps from json
  // In actual testing, the preloaded resources will follow the following branch. The preloaded resources do not deserialize json into an Asset object else if (json.__type__) {
      if (this._depends.has(uuid)) return this._depends.get(uuid);
      var ctor = js._getClassById(json.__type__);
      // Some resources rewrite the _parseDepsFromJson and _parseNativeDepFromJson methods // For example, 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 
  // If there is no __type__ field, its corresponding ctor cannot be found, and the dependency is taken from the __depends__ field of the asset 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 will return an object like this {__isNative__: true, uuid: this._uuid, ext: this._native}
          out.nativeDep = asset._nativeDep;
      }
  }
  // The first time a dependency is found, put it directly into the _depends list, cache dependency list
  this._depends.add(uuid, out);
  return out;
}

The default implementation of CCAsset's _parseDepsFromJson and _parseNativeDepFromJson is as follows. _parseDepsFromJson calls parseDependRecursively to recursively find all __uuid__ of the json object and its sub-objects and put them into the depends array. The implementation of Texture2D, TTFFont, and AudioClip is to directly return an empty array, while the implementation of SpriteFrame is to return cc.assetManager.utils.decodeUuid(json.content.texture) . This field records the uuid of the texture corresponding to SpriteFrame.

_parseNativeDepFromJson will return { __isNative__: true, ext: json._native} if _native of the asset has a value. In fact, most native resources use _nativeDep . The get method of this property will return an object containing something like {__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;
}

3.5 Resource Release

This section focuses on three ways to release resources in Creator and their implementation, and finally introduces how to troubleshoot resource leaks in the project.

3.5.1 Creator Resource Release

Creator supports the following three ways of releasing resources:

Release method Release Effect
Check: Scene->Properties Inspector->Auto Release Resources After the scene is switched, resources not used by the new scene are automatically released
Reference count release res.decRef Use addRef and decRef to maintain the reference count, and automatically release it when the reference count reaches 0 after decRef.
Manual release cc.assetManager.releaseAsset(texture); Manually release resources, forced release

3.5.2 Automatic scene release

When a new scene is running, the Director.runSceneImmediate method will be executed. Here, _autoRelease is called to automatically release the old scene resources (if the old scene has the automatic release of resources checked).

runSceneImmediate: function (scene, onBeforeLoadScene, onLaunched) {
  // Code omitted...
  var oldScene = this._scene;
  if (!CC_EDITOR) {
      // Automatically release resources CC_BUILD && CC_DEBUG && console.time('AutoRelease');
      cc.assetManager._releaseManager._autoRelease(oldScene, scene, persistNodeList);
      CC_BUILD && CC_DEBUG && console.timeEnd('AutoRelease');
  }

  // unload scene
  CC_BUILD && CC_DEBUG && console.time('Destroy');
  if (cc.isValid(oldScene)) {
      oldScene.destroy();
  }
  // Code omitted...
},

The latest version of _autoRelease is very concise and straightforward. It migrates the reference of the persistent node from the old scene to the new scene, and then directly calls the decRef of the resource to reduce the reference count. Whether the resources referenced by the old scene are released depends on whether the old scene has autoReleaseAssets set.

// do auto release
_autoRelease (oldScene, newScene, persistNodes) { 
  // All resources that persistent nodes depend on are automatically addRef and recorded in sceneDeps.persistDeps for (let i = 0, l = persistNodes.length; i < l; i++) {
      var node = persistNodes[i];
      var sceneDeps = dependUtil._depends.get(newScene._id);
      var deps = _persistNodeDeps.get(node.uuid);
      for (let i = 0, l = deps.length; i < l; i++) {
          var dependAsset = assets.get(deps[i]);
          if (dependAsset) {
              dependAsset.addRef();
          }
      }
      if (sceneDeps) {
          !sceneDeps.persistDeps && (sceneDeps.persistDeps = []);
          sceneDeps.persistDeps.push.apply(sceneDeps.persistDeps, deps);
      }
  }

  // Release the old scene's dependencies if (oldScene) {
      var children = dependUtil.getDeps(oldScene._id);
      for (let i = 0, l = children.length; i < l; i++) {
          let asset = assets.get(childs[i]);
          asset && asset.decRef(CC_TEST || oldScene.autoReleaseAssets);
      }
      var dependencies = dependUtil._depends.get(oldScene._id);
      if (dependencies && dependencies.persistDeps) {
          var persistDeps = dependencies.persistDeps;
          for (let i = 0, l = persistDeps.length; i < l; i++) {
              let asset = assets.get(persistDeps[i]);
              asset && asset.decRef(CC_TEST || oldScene.autoReleaseAssets);
          }
      }
      dependUtil.remove(oldScene._id);
  }
},

3.5.3 Reference counting and manual resource release

The remaining two ways of releasing resources are essentially to call releaseManager.tryRelease to release resources. The difference is that decRef decides whether to call tryRelease based on the reference count and autoRelease, while releaseAsset is a forced release. The complete process of resource release is roughly as shown in the following figure:

// CCAsset.js reduce reference decRef (autoRelease) {
  this._ref--;
  autoRelease !== false && cc.assetManager._releaseManager.tryRelease(this);
  return this;
}

// CCAssetManager.js manually releases resources releaseAsset (asset) {
  releaseManager.tryRelease(asset, true);
},

tryRelease supports two modes: delayed release and forced release. When the force parameter is passed in as true, the release process is directly entered. Otherwise, the creator will put the resources into the list to be released and execute the freeAssets method in the EVENT_AFTER_DRAW event to actually clean up the resources. In either case, the resource is passed to the _free method for processing, which does the following.

  • Remove from _toDelete
  • When releasing without force, you need to check whether there are other references, and return if so.
  • Remove from the assets cache
  • Automatically release dependent resources
  • Call the destroy method of the resource to destroy the resource
  • Remove resource dependency records from dependUtil

If the return value of checkCircularReference is greater than 0, it means that the resource is referenced by other places. Other places refer to all the places where we addRef. This method will first record the current refCount of the asset, and then eliminate the references to the asset in the resource and dependent resources. This is equivalent to resource A internally mounting components B and C, which both reference resource A. At this time, the reference count of resource A is 2, and components B and C are actually to be released along with A. However, A is referenced by B and C, so the count is not 0 and cannot be released. Therefore, checkCircularReference first eliminates the internal references. If the refCount of a resource minus the number of internal references is still greater than 1, it means that it is still referenced somewhere else and cannot be released.

tryRelease (asset, force) {
  if (!(asset instanceof cc.Asset)) return;
  if (force) {
      releaseManager._free(asset, force);
  }
  else {
      _toDelete.add(asset._uuid, asset);
      // After the next Director drawing is completed, execute freeAssets
      if (!eventListener) {
          eventListener = true;
          cc.director.once(cc.Director.EVENT_AFTER_DRAW, freeAssets);
      }
  }
}

// Release resources_free (asset, force) {
  _toDelete.remove(asset._uuid);

  if (!cc.isValid(asset, true)) return;

  if (!force) {
      if (asset.refCount > 0) {
          // Check for circular references within the asset if (checkCircularReference(asset) > 0) return; 
      }
  }

  // Remove from cache assets.remove(asset._uuid);
  var depends = dependUtil.getDeps(asset._uuid);
  for (let i = 0, l = depends.length; i < l; i++) {
      var dependAsset = assets.get(depends[i]);
      if (dependAsset) {
          dependAsset.decRef(false);
          releaseManager._free(dependAsset, false);
      }
  }
  asset.destroy();
  dependUtil.remove(asset._uuid);
},

// Release the resources in _toDelete and clear function freeAssets () {
  eventListener = false;
  _toDelete.forEach(function (asset) {
      releaseManager._free(asset);
  });
  _toDelete.clear();
}

What does asset.destroy do? How are resource objects released? How are resources like textures and sounds released? The Asset object itself does not have a destroy method, but the CCObject object inherited by the Asset object implements the destroy method. The implementation here simply puts the object into an array to be released and marks it with ToDestroy . Director calls deferredDestroy to execute _destroyImmediate for resource release in every frame. This method will judge and operate the Destroyed mark of the object, call the _onPreDestroy method to execute the callback, and the _destruct method to destruct.

prototype.destroy = function () {
    if (this._objFlags & Destroyed) {
        cc.warnID(5000);
        return false;
    }
    if (this._objFlags & ToDestroy) {
        return false;
    }
    this._objFlags |= ToDestroy;
    objectsToDestroy.push(this);

    if (CC_EDITOR && deferredDestroyTimer === null && cc.engine && !cc.engine._isUpdating) {
        // Can be destroyed immediately in editor mode deferredDestroyTimer = setImmediate(deferredDestroy);
    }
    return true;
};

// Director calls this method every frame function deferredDestroy () {
    var deleteCount = objectsToDestroy.length;
    for (var i = 0; i < deleteCount; ++i) {
        var obj = objectsToDestroy[i];
        if (!(obj._objFlags & Destroyed)) {
            obj._destroyImmediate();
        }
    }
    // When we call b.destroy in a.onDestroy, the size of the objectsToDestroy array will change. We only destroy the elements in objectsToDestroy before this deferredDestroy if (deleteCount === objectsToDestroy.length) {
        objectsToDestroy.length = 0;
    }
    else {
        objectsToDestroy.splice(0, deleteCount);
    }

    if (CC_EDITOR) {
        deferredDestroyTimer = null;
    }
}

// Real resource release prototype._destroyImmediate = function () {
    if (this._objFlags & Destroyed) {
        cc.errorID(5000);
        return;
    }
    // Execute callback if (this._onPreDestroy) {
        this._onPreDestroy();
    }

    if ((CC_TEST ? (/* make CC_EDITOR mockable*/ Function('return !CC_EDITOR'))() : !CC_EDITOR) || cc.engine._isPlaying) {
        this._destruct();
    }

    this._objFlags |= Destroyed;
};

Here, what _destruct does is to clear the properties of the object, such as setting the properties of object type to null and the properties of string type to ''. The compileDestruct method will return a destructor for the class. compileDestruct first collects all the properties of the two types of ordinary object and cc.Class, and builds a propsToReset according to the type to clear the properties. If JIT is supported, a function return like this will be generated according to the properties to be cleared function(o) {oa='';ob=null;o.['c']=undefined...} , while in the non-JIT case, a function traversed and processed according to propsToReset will be returned. The former takes up more memory but is more efficient.

prototype._destruct = function () {
    var ctor = this.constructor;
    var destruct = ctor.__destruct__;
    if (!destruct) {
        destruct = compileDestruct(this, ctor);
        js.value(ctor, '__destruct__', destruct, true);
    }
    destruct(this);
};

function compileDestruct (obj, ctor) {
    var shouldSkipId = obj instanceof cc._BaseNode || obj instanceof cc.Component;
    var idToSkip = shouldSkipId ? '_id' : null;

    var key, propsToReset = {};
    for (key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (key === idToSkip) {
                continue;
            }
            switch (typeof obj[key]) {
                case 'string':
                    propsToReset[key] = '';
                    break;
                case 'object':
                case 'function':
                    propsToReset[key] = null;
                    break;
            }
        }
    }
    // Overwrite propsToReset according to Class
    if (cc.Class._isCCClass(ctor)) {
        var attrs = cc.Class.Attr.getClassAttrs(ctor);
        var propList = ctor.__props__;
        for (var i = 0; i < propList.length; i++) {
            key = propList[i];
            var attrKey = key + cc.Class.Attr.DELIMETER + 'default';
            if (attrKey in attrs) {
                if (shouldSkipId && key === '_id') {
                    continue;
                }
                switch (typeof attrs[attrKey]) {
                    case 'string':
                        propsToReset[key] = '';
                        break;
                    case 'object':
                    case 'function':
                        propsToReset[key] = null;
                        break;
                    case 'undefined':
                        propsToReset[key] = undefined;
                        break;
                }
            }
        }
    }

    if (CC_SUPPORT_JIT) {
        // compile code
        var func = '';
        for (key in propsToReset) {
            var statement;
            if (CCClass.IDENTIFIER_RE.test(key)) {
                statement = 'o.' + key + '=';
            }
            else {
                statement = 'o[' + CCClass.escapeForJS(key) + ']=';
            }
            var val = propsToReset[key];
            if (val === '') {
                val = '""';
            }
            func += (statement + val + ';\n');
        }
        return Function('o', func);
    }
    else {
        return function (o) {
            for (var key in propsToReset) {
                o[key] = propsToReset[key];
            }
        };
    }
}

So what does _onPreDestroy do? It mainly deregisters various events and timers, and deletes child nodes and components. For details, please see the following code.

//Node's _onPreDestroy
_onPreDestroy () {
  // Calling the _onPreDestroyBase method actually calls BaseNode.prototype._onPreDestroy, which is introduced below var destroyByParent = this._onPreDestroyBase();

  // Logout Actions
  if (ActionManagerExist) {
      cc.director.getActionManager().removeAllActionsFromTarget(this);
  }

  // Remove _currentHovered
  if (_currentHovered === this) {
      _currentHovered = null;
  }

  this._bubblingListeners && this._bubblingListeners.clear();
  this._capturingListeners && this._capturingListeners.clear();

  // Remove all touch and mouse event listeners if (this._touchListener || this._mouseListener) {
      eventManager.removeListeners(this);
      if (this._touchListener) {
          this._touchListener.owner = null;
          this._touchListener.mask = null;
          this._touchListener = null;
      }
      if (this._mouseListener) {
          this._mouseListener.owner = null;
          this._mouseListener.mask = null;
          this._mouseListener = null;
      }
  }

  if (CC_JSB && CC_NATIVERENDERER) {
      this._proxy.destroy();
      this._proxy = null;
  }

  // Recycle into the object pool this._backDataIntoPool();

  if (this._reorderChildDirty) {
      cc.director.__fastOff(cc.Director.EVENT_AFTER_UPDATE, this.sortAllChildren, this);
  }

  if (!destroyByParent) {
      if (CC_EDITOR) {
          // Ensure that in edit mode, the node can be undone by pressing ctrl+z after being deleted (re-adding it to the original parent node)
          this._parent = null;
      }
  }
},

//BaseNode's _onPreDestroy
_onPreDestroy () {
  var i, len;

  // Add the Destroying flag this._objFlags |= Destroying;
  var parent = this._parent;
  
  // Determine whether the release is initiated by destroy of the parent node based on the flag of the parent node var destroyByParent = parent && (parent._objFlags & Destroying);
  if (!destroyByParent && (CC_EDITOR || CC_TEST)) {
      // Remove from editor this._registerIfAttached(false);
  }

  // Release all child nodes, and their _onPreDestroy will also be executed var children = this._children;
  for (i = 0, len = children.length; i < len; ++i) {
      children[i]._destroyImmediate();
  }

  // Release all components, and their _onPreDestroy will also be executed for (i = 0, len = this._components.length; i < len; ++i) {
      var component = this._components[i];
      component._destroyImmediate();
  }

  // Unregister event monitoring, for example, otherNode.on(type, callback, thisNode) registers an event // When thisNode is released, you need to unregister the monitoring on otherNode to avoid event callbacks to the destroyed object var eventTargets = this.__eventTargets;
  for (i = 0, len = eventTargets.length; i < len; ++i) {
      var target = eventTargets[i];
      target && target.targetOff(this);
  }
  eventTargets.length = 0;

  // If it is a permanent node, remove it from the permanent node list if (this._persistNode) {
      cc.game.removePersistRootNode(this);
  }

  // If you release yourself instead of the parent node, notify the parent node to remove the invalid child node if (!destroyByParent) {
      if (parent) {
          var childIndex = parent._children.indexOf(this);
          parent._children.splice(childIndex, 1);
          parent.emit && parent.emit('child-removed', this);
      }
  }

  return destroyByParent;
},

//Component's _onPreDestroy
_onPreDestroy () {
  // Remove ActionManagerExist and schedule
  if (ActionManagerExist) {
      cc.director.getActionManager().removeAllActionsFromTarget(this);
  }
  this.unscheduleAllCallbacks();

  // Remove all listeners var eventTargets = this.__eventTargets;
  for (var i = eventTargets.length - 1; i >= 0; --i) {
      var target = eventTargets[i];
      target && target.targetOff(this);
  }
  eventTargets.length = 0;

  // Stop monitoring in editor mode if (CC_EDITOR && !CC_TEST) {
      _Scene.AssetsWatcher.stop(this);
  }

  // The implementation of destroyComp is to call the onDestroy callback of the component. Each component will destroy its own resources in the callback. // For example, the RigidBody3D component will call the destroy method of the body, and the Animation component will call the stop method cc.director._nodeActivator.destroyComp(this);

  // Remove the component from the node this.node._removeComponent(this);
},

3.5.4 Resource Release Issues

Finally, let's talk about the problem and location of resource release. After adding reference counting, the most common problem is still memory leaks caused by not correctly increasing or decreasing reference counts (circular references, fewer calls to decRef or more calls to addRef), and the problem of resources being released in use (in contrast to memory leaks, resources are released prematurely).

Judging from the current code, if reference counting is used correctly, the new resource bottom layer can avoid problems such as memory leaks.

How to solve this problem? The first step is to locate which resources have problems. If they are released early, we can directly locate this resource. If it is a memory leak, when we find the problem, the program often has occupied a lot of memory. In this case, we can switch to an empty scene and clean up the resources. After cleaning up the resources, we can check whether there are any resources remaining in the assets that have not been released.

To understand why resources are leaked, you can track the calls to addRef and decRef. The following is an example method for tracking the addRef and decRef calls of a resource, and then calling the dump method of the resource to print out the stack of all calls:

public static traceObject(obj : cc.Asset) {
  let addRefFunc = obj.addRef;
  let decRefFunc = obj.decRef;
  let traceMap = new Map();

  obj.addRef = function() : cc.Asset {
      let stack = ResUtil.getCallStack(1);
      let cnt = traceMap.has(stack) ? traceMap.get(stack) + 1 : 1;
      traceMap.set(stack, cnt);
      return addRefFunc.apply(obj, arguments);
  }

  obj.decRef = function() : cc.Asset {
      let stack = ResUtil.getCallStack(1);
      let cnt = traceMap.has(stack) ? traceMap.get(stack) + 1 : 1;
      traceMap.set(stack, cnt);
      return decRefFunc.apply(obj, arguments);
  }

  obj['dump'] = function() {
      console.log(traceMap);
  }
}

The above is the detailed analysis of the new resource management system of CocosCreator. For more information about CococCreator, please pay attention to other related articles on 123WORDPRESS.COM!

You may also be interested in:
  • Unity3D realizes camera lens movement and limits the angle
  • Detailed explanation of how to use several timers in CocosCreator
  • CocosCreator learning modular script
  • How to use physics engine joints in CocosCreator
  • How to use JSZip compression in CocosCreator
  • CocosCreator Getting Started Tutorial: Making Your First Game with TS
  • Interpretation of CocosCreator source code: engine startup and main loop
  • CocosCreator general framework design resource management
  • How to make a List in CocosCreator
  • How to use http and WebSocket in CocosCreator
  • How to use cc.follow for camera tracking in CocosCreator

<<:  Mysql delete duplicate data to keep the smallest id solution

>>:  Detailed steps for building, running, publishing, and obtaining a Docker image for the first time

Recommend

How to start/stop Tomcat server in Java

1. Project Structure 2.CallTomcat.java package co...

Detailed analysis of MySQL instance crash cases

[Problem description] Our production environment ...

WeChat applet implements the Record function

This article shares the specific code for the WeC...

MySQL series multi-table join query 92 and 99 syntax examples detailed tutorial

Table of contents 1. Cartesian product phenomenon...

Steps to create a WEBSERVER using NODE.JS

Table of contents What is nodejs Install NodeJS H...

Overview of the basic components of HTML web pages

<br />The information on web pages is mainly...

Detailed tutorial on installing centos8 on VMware

CentOS official website address https://www.cento...

SQL implementation of LeetCode (183. Customers who have never placed an order)

[LeetCode] 183.Customers Who Never Order Suppose ...

Comparison of several examples of insertion efficiency in Mysql

Preface Recently, due to work needs, I need to in...

Solve the problem of blocking positioning DDL in MySQL 5.7

In the previous article "MySQL table structu...

Teach you how to implement a react from html

What is React React is a simple javascript UI lib...

Example analysis of the principle and solution of MySQL sliding order problem

This article uses examples to explain the princip...

MySQL index failure principle

Table of contents 1. Reasons for index failure 2....