Skip to content

Memory Management

RH edited this page Dec 16, 2024 · 19 revisions

Axmol Memory Management

Origins

Axmol is based on Cocos2d-x, and Cocos2d-x is based on Cocos2d-iphone, which was implemented in Objective-C.

The (old) memory management policy of Objective-C can be read about here

When cocos2d-iphone was ported over to C++ and became Cocos2d-x, the same memory management model was used. Given Axmol is based on Cocso2d-x v4, it has also inherited that same memory management model.

The basics (retain and release)

The majority of classes in Axmol inherit from ax::Object, which handles the reference counting for the instances created from those classes. Reference counting is a way to track how many references there are to a specific resource, which in this case is an object of a class based on ax::Object (such as Node, Sprite etc. etc.).

If you create an object with the new operator, then that object will instantly have a reference count of 1. You can see this in the constructor of ax::Object (see Object.cpp):

Object::Object()
    : _referenceCount(1)  // when the Object is created, the reference count of it is 1
    //...
{
//...
}

If an object is freed via a call to delete, then memory associated with it will be freed, but if areas of the code hold references to this object, then this will result in undefined behaviour (crashes etc.). The reason is that there is no way to notify the holders of those references that the memory associated with that object is no longer valid. So, how do we avoid this situation?

The solution is to use the Object::retain()/Object::release() methods.

Calling retain() on an object will increase its reference count by 1. Calling release() on an object will decrease its reference count by 1. If at any point the reference count becomes 0, the object automatically frees itself, via the delete operator. Refer to the code in void Object::release() (see Object.cpp):

void Object::release()
{
    AXASSERT(_referenceCount > 0, "reference count should be greater than 0");
    --_referenceCount;

    if (_referenceCount == 0)
    {
        //...
        delete this;
    }
}

One very important rule to follow is this: A retain() must always be matched with a release(), otherwise this will result in memory leaks, where objects are not freed from memory, yet no other object holds a reference to them.

So, if you explicitly call retain() on an object, then you must, at some point, also call release() on that same object (when you no longer require it).

You should not explicitly free an object (which inherits from ax::Object) via delete; if the object is no longer required, then simply call release() on it. If no other objects are holding references to it, meaning reference count becomes 0, then that object will free itself.

As an example, you have an object of type Scene, and you need to hold a reference to a sprite in this scene:

class MyScene : public ax::Scene
{
public:

private:
   ax::Sprite* _mySprite; // This is the reference to the sprite
}

Let's say the sprite is created like this (this isn't how you typically create a new instance of a sprite, where you should instead use a create method, but that's covered later):

_mySprite = new Sprite(); // _referenceCount = 1
this->addChild(_mySprite); // _referenceCount = 2, since `addChild()` will call `retain()` on the object

In this case, a retain/release combination is not required, since the number of objects holding a reference to our sprite is the same as the reference count. One reference held in MyScene in _mySprite, and another held by the adding that sprite to the child nodes of that scene. All up that equals 2.

If at any point you no longer require the explicit reference to _mySprite, then simply release it, and set that member variable to nullptr to indicate it is no longer valid.

_mySprite->release(); _referenceCount becomes 1 
_mySprite = nullptr;

At this point the sprite still exists, and has not been freed, because the reference count is 1. That reference is still being held within the scene child node list. When that scene itself is freed (also by a release) and its reference count becomes 0, so the destructor is called, then the scene calls release on all child nodes belonging to it. Any child objects that are released and now have a reference count of 0, such as our example sprite, will automatically be deleted from memory. So far so good... or not quite!

Let's say we do this:

Sprite* sprite = new Sprite(); // _referenceCount = 1
this->addChild(sprite); // _referenceCount = 2, since `addChild` calls `retain()` on the object

We now have a problem. As noted earlier, when the destructor of the scene is called, release() is called on all child nodes of the scene. So, in this case, our example sprite has a reference count of 2, so when release is called, it deducts 1 from the count, so 2 - 1 = 1; the final reference count is 1. The sprite does not delete itself from memory, since its reference count is not yet 0. Nothing else holds a reference to that sprite, so now we've lost all links to that sprite and the memory associated with it. We have no way to delete it from memory, so we've just lost that bit of memory. This is the start of a very nasty memory leak; the more objects we create via new, and don't explicitly delete them, the more memory we lose, until eventually we run out of memory.

So, what is the solution to this? Do we need to hold references and call delete on each and every object we create via new? .... do we really want to add so much more code that required to track each and every object of type ax::Object, and then figure out if that object reference count is equal to 1 in order to explicitly call delete... that's just nasty. So, no, there is another bit to this puzzle, a method called autorelease() (in Object.cpp).

autorelease() to the rescue

The purpose of this call is to add an object (of type ax::Object) to a list, and at the end of the current main loop cycle, release() is called on each and every item in that list. This only happens once, since the after these objects are released, the list is cleared, so they no longer exist in that list.

Why would we want to do this? The best way is to show you an example, so let us use the same sprite code as earlier:

Sprite* sprite = new Sprite(); // _referenceCount = 1
sprite->autorelease(); // adds this object to the auto-release pool (the list of objects to be released on the next cycle of the main loop)
this->addChild(sprite); // _referenceCount = 2, since `addChild` calls `retain()` on the object

As you can see, all we have done is added the call to sprite->autorelease(); after we created a new instance of the sprite via new. So, what does this actually do?

Let's think of this in main loop cycles:

  • Current main loop cycle, we create the sprite and add it to our scene. At this point, the reference count of the sprite is equal to 2 (as you can see above)
  • At the end of the current main loop, just prior to starting the next loop, the auto-release pool is iterated through, and release() is called on every object in that list. Since this list contains a reference to our sprite (currently at _referenceCount = 2), once release() is called on it, the reference count becomes 1. The auto-release pool is then emptied, so it no longer holds a reference to our sprite.

So, now what?

We know the sprite now has a reference count of 1. As we mentioned earlier, if the parent scene of this sprite is freed from memory (the destructor of the scene is called), then release() will be called on every child node in that scene. Since our sprite is at a reference count of 1, when release is called on it, that reference count becomes 0, and we know that when it is 0, the sprite will free itself automatically. The memory associated with the sprite is freed correctly, so no more memory leak!

So, what do know this far:

  • If an object of type ax::Object is created via the new operator, then it must be freed by a call to delete. We do not want to manage this manually, so...
  • In order to avoid managing the lifetime of objects manually, we add newly created objects to the auto-release pool via a call to autorelease(), to automatically call release on objects at the end of our current main loop cycle . This will ensure that the object is freed correctly if nothing is holding a reference to it (so reference count is 0).

By convention, you would typically see static methods named create**** in all classes inheriting from ax::Object. These are convenience methods that create a new instance of that class, and automatically call autorelease() on that new instance, reducing the amount of code we need to write, and saving us from being forgetting to call autorelease() ourselves; basically, less boilerplate and error-prone code.

Now, just so you are aware, in certain cases we may not want to call autorelease() on objects, but that's because we know that we only need a single reference to that object, and we are well aware of needing to free that object when it is no longer required. Only do this if you know what you're doing, and if you have a specific reason to be doing this.

NOTE: If for some reason you need to call autorelease more than once in the SAME main-loop cycle (basically, never, perhaps a programming mistake), then the rule is that there must be 1 reference for each call to autorelease(). If you call autorelease() 3 times on an object, then that object must have a reference count of 3 or more.

For example:

We know that a new on an object of type ax::Object always has an initial reference count of 1. We can call autorelease() on this. If we want to call autorelease() again, then we would need to call retain() to increase the reference count by 1 as well.

Sprite* sprite = new Sprite(); // _referenceCount = 1
sprite->autorelease(); // will delete 1 reference at the end of the main loop
sprite->retain(); // _referenceCount = 2
sprite->autorelease(); // will delete 1 reference at the end of the main loop
sprite->retain(); // _referenceCount = 3
sprite->autorelease(); // will delete 1 reference at the end of the main loop

So, when it gets to the end of the main loop, this happens (pseudo-code):

foreach (item in list)
{
    item->release();  // _referenceCount = _referenceCount - 1
}

Since our example sprite is in the list 3 times, then release() is called 3 times on it, so the end result is that it equals 0, so it will be freed, since we are not using it anywhere else (nothing else holds a reference to it).

Once again, there should be no reason at all to add it to the auto-release pool more than once within the same main loop, and if that does happen, then it is most likely a coding error.

In summary:

  • Use the static create methods to create new instances of classes inheriting from ax::Object. In your own sub-classes of Axmol ax::Object objects (Node, Sprite etc), ensure that you implement these create methods too. The create method should (almost always) have a call to autorelease() on that new object.
  • All sub-classes of Node will automatically call retain() and release() on child nodes (added via addChild() etc.). We do not need to explicitly retain or release these objects.
  • If we need to hold a reference to a ax::Object object for some particular reason, then we must call retain() when we store the reference, and release() when we no longer require it. For example:
class MyNode : public ax::Node
{
public:
    ~MyNode(); // destructor
    bool init() override;

private:
    ax::Sprite* _mySprite = nullptr;
}

Implementation of MyNode:

bool MyNode::init() // In current main loop cycle init() is called:
{
    //...
    _mySprite = Sprite::create(); // _referenceCount = 1, added to auto-release pool
    _mySprite->retain(); // _referenceCount = 2 (in auto-release pool)
    addChild(_mySprite); // _referenceCount = 3 (in auto-release pool)
    //...
}

MyNode::~MyNode() // Destructor
{
   if (_mySprite != nullptr)
       _mySprite->release(); // _referenceCount = _referenceCount - 1
}

On the next main loop cycle, the auto-release pool is processed, so 1 is deducted from all objects in that pool. After this, _mySprite will now have a reference count of 2, which is correct, since it's added as a child of MyNode, and we also hold an explicit reference to it in _mySprite; a total of 2 references. These match with the 2 release() calls that will occur, one if that child is removed from the list of children in MyNode (such as MyNode::removeAllChildren() etc.), and another when we call _mySprite->release() if the MyNode destructor is called.

Convenience Macros

To reduce the amount of code used to retain, release and set a variable to null, there are a few macros to use:

  • AX_SAFE_RELEASE(object) = If object is not null, then this calls object->release()
  • AX_SAFE_RELEASE_NULL(object) = If object is not null, then this calls object->release() and sets object = nullptr
  • AX_SAFE_RETAIN(object) = If object is not null, then this calls object->retain()

More macros can be found in PlatformMacros.h.

Detecting memory leaks and dangling references

Diagnostic functionality exists that you can enable during development to help with detecting memory leaks and dangling references of ax::Object objects. This comes in the form of the AX_REF_LEAK_DETECTION preprocessor define (see Object.h):

#define AX_REF_LEAK_DETECTION 0

When AX_REF_LEAK_DETECTION is set to 1, all Axmol object references will be tracked, which gives us the ability to check for a few different issues.

Catching memory leaks

The reference count is tracked, so at any point, you may call the static method Object::printLeaks() to print out a list of all objects remaining in the tracking list. An object would only be in the list if its reference count is greater than 0, meaning it has not yet been freed. The Object::printLeaks() method can be called at any time, but that wouldn't be really useful. Where you want to call this is as close to the last object to be destroyed when closing the application. For example, for Windows, MacOS and Linux builds, it would be after the application instance has been freed:

int axmol_main() {
    // create the application instance
    AppDelegate app;
    int ret = Application::getInstance()->run();
    return ret;
}

int main(int, char**) {
    auto result = axmol_main();

#if AX_OBJECT_LEAK_DETECTION
    Object::printLeaks();
#endif

    return result;
}

On application exit, you will get a list of objects that have not yet been freed. Generally, this would mean that release() was never called on those objects. The higher the reference count shown for an object, the more areas of code holding a reference to it.

Dangling references

When memory is being freed for a ax::Object object, the reference count is checked to ensure it is equal to 0, since nothing should be holding onto a reference to this object (for example, a pointer to it).

If the reference count is not equal to zero, then something is still be holding a reference to this object. What this means is that when the memory is freed, any reference to that object will now no longer be pointing to valid content, and any attempt to use that reference will result in undefined behaviour (such as a crash of the app).

This situation is usually caused by either accidentally using the delete operator on a ax::Object object, or calling release() without a prior matching retain().

NOTE: If required, AX_REF_LEAK_DETECTION may be enabled in a release build, but only do this if and only if you absolutely need to, and disable it when you no longer need it.

RefPtr: Yet more convenience

More convenience at a (subjectively) low price, since there is some overhead.

The easiest mistake to make is forgetting to add a release() call for every retain() used, or vice versa (although rarer).

You can avoid this situation in your own code if you use the ax::RefPtr, which is a relatively smart pointer, implemented in such a way to be similar to the std::shared_ptr. What this does is handle the retain() and release() for you automatically.

For example:

Sprite*_mySprite = nullptr;

_mySprite = Sprite::create();
_mySprite->retain();
// do some stuff with this sprite
// then later...
_mySprite->release(); // Finally releases the reference, and if it happens to equal 0, it is freed from memory
_mySprite = nullptr;

or with macros:

Sprite*_mySprite = nullptr;

_mySprite = Sprite::create();
AX_SAFE_RETAIN(_mySprite);
someParentNode->addChild(_mySprite);
// do some stuff with this sprite
// then later...
AX_SAFE_RELEASE_NULL(_mySprite); // Finally releases the reference, and if it happens to equal 0, it is freed from memory

With RefPtr, the above code is changed to the following:

ax::RefPtr<Sprite>_mySprite;

_mySprite = Sprite::create(); // RefPtr automatically calls `retain()`
someParentNode->addChild(_mySprite); // Use it as you normally would
// do some stuff with this sprite
// then later...
_mySprite = nullptr; // RefPtr automatically calls `release()` and becomes a nullptr to no longer point at any memory

As you can see from the above example, this addresses both situations of dangling references and memory leaks.

If you're thinking, "Why isn't this used throughout the Axmol engine code?", well, as mentioned earlier, there are overheads to using the ax::RefPtr, the specifics of which may be a topic for another article. What is important is getting the most performance out of Axmol, so writing code to call retain() and release() for memory management of objects is the most appropriate thing to do in order to achieve that goal.

Custom Axmol containers with built-in reference management

There are several containers implemented in Axmol which serve to hold Axmol objects (sub-classes of ax::Object). These containers will automatically call retain() and release() on object insertion and removal respectively.

They are:

  • ax::Vector<ax::Object*> (see Vector.h): Holds a lit of items, similar to std::vector
  • ax::Map<Key, ax::Object*> (see Map.h): Holds key/value pairs, similar to std::unordered_map. The Axmol object must be stored in the value field, not the key field.

If you need to store a list of objects, for example, a bunch of sprites, in order to quickly access them later without having to search the child nodes of their parent object, then you may want to store them in a container. Let's say that you want to use a key/value store, so a map of items. If you attempted to use std::vector, and to ensure there are no dangling references, you would write code similar to this:

std::vector<Sprite*> _sprites;

Sprite* sprite1 = Sprite::create();
addChild(sprite1);
_sprites.push_back(sprite1);
sprite1->retain();

Sprite* sprite2 = Sprite::create();
addChild(sprite2);
_sprites.push_back(sprite2);
sprite2->retain();

When you no longer need the sprites in the container, then you would do something like this:

for (auto&& sprite : _sprites)
{
    sprite->release();
}

_sprites.clear(); // You have released all the sprites, so clear the container

Now, if you forget to call retain() or release(), you will end up with problems, as described previously.

To avoid these issues, and write less code, you can instead use the custom Axmol containers. For the example above, we will use ax::Vector:

ax::Vector<Sprite*> _sprites;

Sprite* sprite1 = Sprite::create();
addChild(sprite1);
_sprites.pushBack(sprite1); // Note that we no longer need to call retain(), since it's done for us within this container

Sprite* sprite2 = Sprite::create();
addChild(sprite2);
_sprites.pushBack(sprite2);

When you no longer need the list of sprites, empty the list:

_sprites.clear(); // This automatically calls release() on all the items in the list, then empties the list.

The same applies if you remove specific entries from the list. For example, if we only want to remove the first sprite in the list, then we simply call:

_sprites.erase(0); // Remove first entry in the list. This automatically calls release() on the sprite object being removed