1. Resources and Construction1.1 creator resource file basicsBefore 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:
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 { "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 ConstructionAfter 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
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 { "__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. 1.2.2 Prefab and Scene
Scene resources are very similar to Prefab resources. They are both json files that describe all nodes, components, and other information. When 1.2.3 Resource file merging rules When Creator merges multiple resources into one json file, we can find the
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.
The default option is a good choice in most cases. If it is a web platform, it is recommended to check 2. Understanding and using Asset Bundle2.1 Create a BundleAsset 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:
Creating an Asset Bundle is very simple. Just check the box in the directory's 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
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:
// 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:
3. Analysis of the new resource frameworkThe 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
Bundle
Pipeline part CCAssetManager.js manages the pipeline and provides a unified loading and unloading interface Pipeline framework
Pretreatment pipeline
Download pipeline
Parsing pipeline
other
3.1 Loading pipelineCreator 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)
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.
For other resources If uuid is in 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
// 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:
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
File class, which can be divided into binary files, json files and text files
Font class loadFont builds css style and specifies url to download Sound class downloadAudio
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 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 parsingIn 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 3.4 Dependency LoadingCreator 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 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:
// 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:
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(json) { var depends = []; parseDependRecursively(json, depends); return depends; }, _parseNativeDepFromJson(json) { if (json._native) return { __isNative__: true, ext: json._native}; return null; } 3.5 Resource ReleaseThis 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:
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
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 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 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 //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:
|
<<: Mysql delete duplicate data to keep the smallest id solution
1. Project Structure 2.CallTomcat.java package co...
[Problem description] Our production environment ...
In the past two days, I have been very troubled t...
This article shares the specific code for the WeC...
Table of contents 1. Cartesian product phenomenon...
Table of contents What is nodejs Install NodeJS H...
<br />The information on web pages is mainly...
CentOS official website address https://www.cento...
[LeetCode] 183.Customers Who Never Order Suppose ...
Preface Recently, due to work needs, I need to in...
In the previous article "MySQL table structu...
1 Download The address is: https://dev.mysql.com/...
What is React React is a simple javascript UI lib...
This article uses examples to explain the princip...
Table of contents 1. Reasons for index failure 2....