Version: 3.4.0
Language: TypeScript
Environment: Mac
Review
There are two previous blogs that explain:
- cocosCreator’s resources dynamic loading and preloading tells about static reference resources, dynamic loading and preloading.
- cocosCreator’s Bundle talks about the use of AssetManager’s built-in Bundles and custom Bundles
The simple understanding is related to the use of static and dynamic reference resources in cocosCreator. In order to make it easier to manage dynamic resources, AssetManager
is added for Management and release of resources.
Dynamically referenced resources and related interfaces are all asynchronous operations
When it comes to resource management, it involves memory management of resources.
In cocosCreator, the official has different memory management methods for different resources. There are:
- Static reference resources, automatic management and release through serialized data
- Dynamically reference resources, increase reference count management to avoid incorrect releases, and release management of resources by
AssetManager
- Automatic release management of scenes
In essence, they are all citation counting, but in order to have a better understanding, they are summarized through this blog.
My understanding may be wrong, please point it out.
Reference Count
The resources in cocosCreator are placed in the assets directory. The main sources are:
- Import from outside
- Resources downloaded remotely
They will eventually be wrapped so that they inherit from the resource base class: Asset
.
object
event handling
Resource base class
cocos_core_assets_asset_Asset_base
CCObject
cocos_core_event_eventify_IEventified
Asset
In cocosCreator, the important role of Asset
is to reference count the resources. The main definitions are as follows:
//cc.d.ts export class Asset extends __private.cocos_core_assets_asset_Asset_base {<!-- --> //The URL of the target platform resource corresponding to this resource. If not, an empty string will be returned. get nativeUrl(): string; // serialize object serialize(): void; // Get the number of references get refCount(): number; //Increase reference count addRef(): Asset; // Reduce the resource reference and try to automatically release it decRef(autoRelease?: boolean): Asset; } // Main implementation: ../resources/3d/engine/cocos/core/assets/asset.ts export class Asset extends Eventify(CCObject) {<!-- --> private_ref = 0; //Reference count number public get refCount (): number {<!-- --> return this._ref; } // reference count + 1 public addRef (): Asset {<!-- --> this._ref + + ; return this; } //Reference count -1 and attempt automatic release public decRef (autoRelease = true): Asset {<!-- --> if (this._ref > 0) {<!-- --> this._ref--; } //Check whether to release automatically if (autoRelease) {<!-- --> legacyCC.assetManager._releaseManager.tryRelease(this); } return this; } }
For the automatic release interface tryRelease under decRef
, let’s take a look at the general implementation:
// ../resources/3d/engine/cocos/core/asset-manager/release-manager.ts class ReleaseManager {<!-- --> private _eventListener = false; //Array of resources to be released private _toDelete = new Cache<Asset>(); //Try to automatically release (release the object, whether to force release is false by default) public tryRelease (asset: Asset, force = false): void {<!-- --> if (!(asset instanceof Asset)) {<!-- --> return; } // If forced release, release the resource if (force) {<!-- --> this._free(asset, force); return; } // If there is no forced release, cache the uuid of the object into the resource object to be released. this._toDelete.add(asset._uuid, asset); // Check whether the object has registered an event listener. If it is not registered, the release of resources will be detected in the next frame. if (!this._eventListener) {<!-- --> this._eventListener = true; callInNextTick(this._freeAssets.bind(this)); } } // Next frame release detection for event listeners private _freeAssets () {<!-- --> this._eventListener = false; this._toDelete.forEach((asset) => {<!-- --> this._free(asset); }); // Note: Clearing is used to ensure that the cached object is only traversed once, that is, the life cycle is only one frame. this._toDelete.clear(); } // Release the object (object, whether to force release) private _free (asset: Asset, force = false) {<!-- --> const uuid = asset._uuid; //Remove the released object from the cache this._toDelete.remove(uuid); // Check whether the object is valid if (!isValid(asset, true)) {<!-- --> return; } \t\t if (!force) {<!-- --> // Check the reference count and whether there is a circular reference, return if it exists if (asset.refCount > 0) {<!-- --> if (checkCircularReference(asset) > 0) {<!-- --> return; } } } //Remove object from cache assets.remove(uuid); // Get all dependencies of the resource through uuid and traverse const depends = dependUtil.getDeps(uuid); for (let i = 0, l = depends.length; i < l; i + + ) {<!-- --> // If the object is valid, then the reference count is -1 const dependAsset = assets.get(depends[i]); if (dependAsset) {<!-- --> dependAsset.decRef(false); // no need to release dependencies recursively in editor if (!EDITOR) {<!-- --> this._free(dependAsset, false); } } } // ... } }
Its process introduction:
- If the object is not forced to be released, it is stored in a temporary array, and the array object in the cache is traversed in the next frame to perform the release operation.
- If the object is forcibly released, call the release interface
- The release interface will remove the object from the temporary array and check whether the object is valid and referenced
- If the object can be removed, get the dependencies and traverse for reference count -1
- When the reference count is 0, the object is released.
Here are a few things to note:
- For
this._eventListener
it is a tag, which is mainly used to ensure that the object needs to be executed in the next frame - The operation
this._toDelete.clear()
is added to the object in the release operation, mainly to ensure that the life cycle of the object is only one frame.
For the latter, the life cycle callback has only one frame, much like the memory management processing in cocos2d-x:
//In the while main loop of application.cpp, mainLoop is called every frame according to FPS void Director::mainLoop() {<!-- --> if (! _invalid) {<!-- --> drawScene(); // Clean up the current release pool object PoolManager::getInstance()->getCurrentPool()->clear(); } } void AutoreleasePool::clear() {<!-- --> // By using the vector.swap method for exchange, it is guaranteed that the node data is only traversed once per frame. std::vector<Ref*> releasings; releasings.swap(_managedObjectArray); // Traverse all objects, perform a reference count of -1, and destroy the object if it is 0 for (const auto & amp;obj : releasings) {<!-- --> obj->release(); } }
For information about the memory mechanism of cocos2d-x, please refer to: cocos2d-x memory management mechanism
Many resources in cocosCreator are interdependent, and their reference counting structures are similar to the following:
-
When a resource is used, the reference count is:
-
A reference to a resource is added. The resource has dependencies. The reference count is:
-
Release resource A, the reference count is:
If the reference count is 0, the release operation is performed.
Dynamic Reference
Static referenced resources will be serialized by the compiler and recorded in the serialized data. The engine can count the reference relationships, so there is no need to pay attention to the release of memory.
However, dynamically referenced resources can be used flexibly and loaded when needed.
Because there is no serialization, the engine cannot count reference relationships. As a result, the reference count is 0, and the problem of being accidentally released may occur.
Therefore, you need to use the interfaces of addRef()
and decRef()
for manual management:
const url = 'img_bag/spriteFrame'; resources.load(url, SpriteFrame, (err, spriteFrame) => {<!-- --> if (err) {<!-- --> return console.err(err.message); } let sprite = this.node.getComponent(Sprite); sprite.spriteFrame = spriteFrame; //Increase the reference count to ensure that resources are not released incorrectly spriteFrame.addRef(); this._spriteFrame = spriteFrame; }); //When the node is destroyed protected onDestory() {<!-- --> if (this._spriteFrame) {<!-- --> this._spriteFrame.decRef(); this._spriteFrame = null; } }
Note: When used in pairs, especially addRef, if called frequently, the reference count is very likely to be non-zero and memory wasted.
AssetManager
The officially provided AssetManager
module is responsible for loading and releasing resources. If you forget to use reference counting in the above example, there will still be a memory leak problem.
The main interfaces provided by AssetManager
for memory management are:
export class AssetManager {<!-- --> // Collection of loaded bundles bundles: AssetManager.Cache<AssetManager.Bundle>; // Get Bundle getBundle(name: string): AssetManager.Bundle | null; //Remove Bundle removeBundle(bundle: AssetManager.Bundle): void; // Collection of loaded resources assets: AssetManager.Cache<Asset>; // Releasing the resource and its dependent resources will not only delete the cache reference of the resource from the assetManager, but also clean up its resource content. releaseAsset(asset: Asset): void; // Release all unused resources releaseUnusedAssets(): void; // Release all resources releaseAll(): void; }
Note: As long as the Bundle is managed by AssetManager, the Bundle and the removal of resources within the Bundle are two different things.
If you want to remove Bundle after it is no longer used, you need to first release the resources in Bundle.
let bundle = assetManager.getBundle("test_bundle"); if (!bundle) {<!-- --> return; } // Release all resources in the bundle bundle.releaseAll(); //Remove Bundle assetManager.removeBundle(bundle);
Regarding the release of Asset resources by AssetManager
, let’s take a look at the main implementation of the engine:
// ../resources/3d/engine/cocos/core/asset-manager/asset-manager.ts export class AssetManager {<!-- --> public releaseAsset (asset: Asset): void {<!-- --> releaseManager.tryRelease(asset, true); } public releaseUnusedAssets () {<!-- --> assets.forEach((asset) => {<!-- --> releaseManager.tryRelease(asset); }); } public releaseAll () {<!-- --> assets.forEach((asset) => {<!-- --> releaseManager.tryRelease(asset, true); }); } }
For the specific implementation of releaseManager.tryRelease, see the display of release-manager.ts above.
In addition to the resource release provided by AssetManager
, there are also some release interfaces in Bundle, which are mainly used to release a single resource.
let bundle = assetManager.getBundle("test_bundle"); if (!bundle) {<!-- --> return; } // Release a single resource in the bundle bundle.release(`image`, SpriteFrame); //Remove Bundle assetManager.removeBundle(bundle);
The main implementation code in the engine:
// ../resources/3d/engine/cocos/core/asset-manager/bundle.ts export default class Bundle {<!-- --> // Release the resources at the specified path in the package public release (path: string, type?: AssetType | null) {<!-- --> const asset = this.get(path, type); if (asset) {<!-- --> releaseManager.tryRelease(asset, true); } } // Release unused resources in the package public releaseUnusedAssets () {<!-- --> assets.forEach((asset) => {<!-- --> const info = this.getAssetInfo(asset._uuid); if (info & amp; & amp; !info.redirect) {<!-- --> releaseManager.tryRelease(asset); } }); } // Release all resources in the package public releaseAll () {<!-- --> assets.forEach((asset) => {<!-- --> const info = this.getAssetInfo(asset._uuid); if (info & amp; & amp; !info.redirect) {<!-- --> releaseManager.tryRelease(asset, true); } }); } }
For the specific implementation of releaseManager.tryRelease, see the display of release-manager.ts above.
Scene release
For automatically releasing resources, there is a parameter called AutoReleaseAssets
in the scene’s Attribute Inspector, check it.
When the scene is switched, all dependent resources in the scene will be automatically released.
Main logic implementation:
- When
director.loadScene
ordirector.runScene
, they will call therunSceneImmediate
method - This method will call the interface
_autoRelease
under release-manager.ts
// ../resources/3d/engine/cocos/core/asset-manager/release-manager.ts // The scene's automatic release mark autoReleaseAssets // If it is true, it means that the reference count will be automatically released after -1, that is, the tryRelease interface will be called. public _autoRelease (oldScene: Scene, newScene: Scene, persistNodes: Record<string, Node>) {<!-- --> // Check if there are old scenes if (oldScene) {<!-- --> const children = dependUtil.getDeps(oldScene.uuid); for (let i = 0, l = children.length; i < l; i + + ) {<!-- --> const asset = assets.get(childs[i]); if (asset) {<!-- --> //Important code, if it is true, call the tryRelease interface asset.decRef(TEST || oldScene.autoReleaseAssets); } } const dependencies = dependUtil._depends.get(oldScene.uuid); if (dependencies & amp; & amp; dependencies.persistDeps) {<!-- --> const persistDeps = dependencies.persistDeps; for (let i = 0, l = persistDeps.length; i < l; i + + ) {<!-- --> const asset = assets.get(persistDeps[i]); if (asset) {<!-- --> //Important code, if it is true, call the tryRelease interface asset.decRef(TEST || oldScene.autoReleaseAssets); } } } if (oldScene.uuid !== newScene.uuid) {<!-- --> dependUtil.remove(oldScene.uuid); } } // ... }
Summary
Resource release of cocosCreator, finally summarized:
- Resource-related memory management is reference counting, managed through
Asset
- Logical operations related to reference counting are in
release-manager.ts
- The main code idea of automatic release is to save the released object into a temporary array, and the life cycle of the temporary array is only one frame.
- Depending on the scenario, it is recommended to check the AutoReleaseAssets option to automatically release memory.
- Related to Bundle, it is recommended to use the interfaces of
release
,releaseUnusedAssets
, andreleaseAll
appropriately. - Related to AssetManager, when releasing the Bundle, pay attention to the call of the resource release interface (same as the Bundle name)
Finally, I wish you all a happy study and life!