AssetManager resource management and release of cocosCreator

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:

  1. For this._eventListener it is a tag, which is mainly used to ensure that the object needs to be executed in the next frame
  2. 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:

  1. When a resource is used, the reference count is:
    Please add image description

  2. A reference to a resource is added. The resource has dependencies. The reference count is:
    Please add image description

  3. Release resource A, the reference count is:
    Please add image description

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.
Please add image description

Main logic implementation:

  1. When director.loadScene or director.runScene, they will call the runSceneImmediate method
  2. 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:

  1. Resource-related memory management is reference counting, managed through Asset
  2. Logical operations related to reference counting are in release-manager.ts
  3. 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.
  4. Depending on the scenario, it is recommended to check the AutoReleaseAssets option to automatically release memory.
  5. Related to Bundle, it is recommended to use the interfaces of release, releaseUnusedAssets, and releaseAll appropriately.
  6. 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!