Memory Management
Managing memory in general with C++ has been partially covered in C++ tips, but Cocos2d comes with its own memory management system that is all modders have to learn at some point. On top of this, Geode adds two classes, Ref
and WeakRef
, which Geode devs will tell you to use but not how to use them. This tutorial aims to remedy that, explaining how memory management in Cocos2d works in detail.
Memory management
Cocos2d’s memory management features are available to all classes that inherit from CCObject
, as it is the base class that implements the required features. Cocos2d’s memory management works by ref counting - in other words, it just stores how many references exist to an object, and once the ref count reaches 0, the object’s memory is freed.
Unfortunately, due to how C++ works, the ref counting is not directly automatic - it doesn’t know where the object is actually used, only the ref count stored inside it. In practice, classes like CCNode
always handle retaining and releasing their children, so you usually don’t have to deal with memory management when building nodes. However, this does have consequences when it comes to class members, as we will see later.
The easiest way to concretize this is to look at manual memory management in Cocos2d, which works by using the retain
and release
functions. In their simplicity, retain
increments the object’s ref count by one, and release
decrements it by one.
// Ref count of a created object is always 1
auto node = CCNode::create();
// Increment ref count to 2
node->retain();
// Decrement ref count to 1, and then to 0, freeing the object
node->release();
node->release();
You can think of retain
and release
like the following: retain
tells Cocos2d “Hey, I’m still using this object, please don’t free it!”, and release
tells it “Okay, I don’t need this object anymore!”
In practice, if you have to do manual memory management, you likely won’t be using the retain
and release
functions directly and instead use the CC_SAFE_RETAIN
and CC_SAFE_RELEASE
macros:
// Increment ref count
CC_SAFE_RETAIN(node);
// Decrement ref count
CC_SAFE_RELEASE(node);
The difference these macros have over direct function calls is that they check that the object isn’t nullptr
first.
⚠️ You should never call
release
more than the amount of times you have calledretain
on an object to force-free it, as if some other code holds a pointer to the object and is expecting it to be retained, you will cause that code to break.
Classes
Remembering to manage every object you have ever created is a real problem, though. Luckily, most Cocos2d classes call retain
for you.
auto node = CCNode::create();
auto other = CCNode::create();
// CCNode increments other's ref count by 1, and decrements it when the child
// is removed or when node is freed
node->addChild(other);
auto array = CCArray::create();
// CCArray increments node's ref count by 1, and decrements it when the node
// is removed or when the array is freed
array->addObject(node);
This means that if you store your created object as a child to something or add it to an array, you don’t have to worry about memory management. However, if you don’t do that, you need to be careful:
{
auto array = CCArray::create();
// If this array is never used anywhere, its ref count stays at 1 and
// nothing calls release on it, so the memory is leaked - except it's not!
}
This code looks like it should cause a memory leak, since nothing ever calls release
on the array. However, Cocos2d has an automatic garbage collector that handles situations like this. You may have seen that all create
functions in nodes call a function called autorelease
:
SomeNode* SomeNode::create() {
auto ret = new SomeNode();
if (ret->init()) {
// Make the node be automatically garbage collected
ret->autorelease();
return ret;
}
delete ret;
return nullptr;
}
This tells Cocos2d that if no calls to retain
are made to this object, it should be freed automatically later on. This means that the following code:
{
auto array = CCArray::create();
}
does not cause a memory leak, as Cocos2d notices no retain
calls to array
have been made, and frees it.
However, the array is not freed immediately; instead, it is usually freed at earliest on the next frame. This means that you can still use the array to do things:
auto array = CCArray::create();
array->addObject(...);
someNode->someFunction(array);
and it will be automatically freed later on.
Class members
autorelease
is very neat when you just want to quickly create an array to pass into a function, or something similar. However, it causes a bit of a headache when dealing with class members:
class MyNode : public CCNode {
protected:
CCArray* m_array;
bool init() {
if (!CCNode::init())
return false;
m_array = CCArray::create();
return true;
}
public:
CCObject* getFirst() const {
return m_array->firstObject();
}
};
If you ran this code, you would notice that your mod mysteriously crashes in any call made to getFirst
after a while. The reason for this is that m_array
has been freed - no calls to retain
were ever made to it, and since Cocos2d ref counting doesn’t actually have any knowledge of what pointers to an object exist, Cocos2d concludes that the array must not be in use.
The solution, then, is to call retain
on the array:
class MyNode : public CCNode {
protected:
CCArray* m_array;
bool init() {
if (!CCNode::init())
return false;
m_array = CCArray::create();
m_array->retain();
return true;
}
public:
CCObject* getFirst() const {
return m_array->firstObject();
}
};
Now Cocos2d knows that m_array
is actually being used, and won’t free it. However, now we have caused a memory leak: when MyNode
is destroyed, the array’s ref count stays at 1, meaning it never gets freed. To fix this, we need to add a call to release
in MyNode
’s destructor:
class MyNode : public CCNode {
protected:
CCArray* m_array;
bool init() {
if (!CCNode::init())
return false;
m_array = CCArray::create();
m_array->retain();
return true;
}
virtual ~MyNode() {
m_array->release();
}
public:
CCObject* getFirst() const {
return m_array->firstObject();
}
};
This is the conventional pattern for working with CCObject
members, and you may find yourself reading old code that uses it, or even using it yourself. However, it comes with a problem: this gets really complex to work with really fast. Imagine that m_array
is not just a member that is created once at the start and freed at the end, but instead may be nullptr
, or may be removed later, or may even be swapped with other arrays. Dealing with ref counting manually in that case is a real pain:
class MyNode : public CCNode {
protected:
CCArray* m_array = nullptr;
bool init() {
if (!CCNode::init())
return false;
m_array = CCArray::create();
m_array->retain();
return true;
}
virtual ~MyNode() {
CC_SAFE_RELEASE(m_array);
}
public:
void swapArray(CCArray* other) {
// Release existing array
CC_SAFE_RELEASE(m_array);
// Swap array
m_array = other;
// Retain the new array
CC_SAFE_RETAIN(m_array);
}
void removeArray() {
CC_SAFE_RELEASE(m_array);
m_array = nullptr;
}
};
When you add in more members, trying to keep track of all these relations gets really complex, and the possibility of an accidental memory leak / memory issue increases exponentially. Luckily, Geode has a solution: Ref
.
Ref
Ref
is a smart pointer for CCObject
s - in essence, it’s just a class that retains the object it points to, and releases it when Ref
goes out of scope. In other words, it’s a RAII alternative to manual retain
and release
calls. Using it, we can refactor our previous code to just this:
class MyNode : public CCNode {
protected:
Ref<CCArray> m_array = nullptr;
bool init() {
if (!CCNode::init())
return false;
m_array = CCArray::create();
return true;
}
public:
void swapArray(CCArray* other) {
// Swap array
m_array = other;
}
void removeArray() {
m_array = nullptr;
}
};
Notice that all manual calls to retain
and release
disappeared from the code - Ref
handles all of them for you. This makes writing and reasoning about code much simpler - you can be assured that as long as you have initially made a Ref
to a valid object, it’s always going to stay valid, and the memory will be freed appropriately when you actually no longer use it.
Ref
is also relatively cheap - if you are unsure whether the lifetime of a pointer you have extends to your usage, just store the pointer in a Ref
to stay safe.
In general, you should at least stick Ref
to all your class members, unless you can be certain about the lifetime of the object otherwise.
WeakRef
Ref
does have a problem, however: since it increments the ref count, the object being pointed to will only be freed once the Ref
goes out of scope. However, sometimes this won’t happen at a desirable time: for example, some mod might have a map like the following:
static std::unordered_map<CCNode*, SomeData> REGISTERED_NODES {};
void addDataToNode(CCNode* node, SomeData const& data) {
REGISTERED_NODES.insert({ node, data });
}
void doSomethingWithNodes() {
for (auto& [node, data] : REGISTERED_NODES) {
// ...
}
}
For example, a right mouse click API might store the nodes that have registered themselves as mouse right click event listeners like this. However, this code is never told when the CCNode
pointer it has is no longer valid, so if the scene is changed and the nodes are freed while they are still in REGISTERED_NODES
, the next call to doSomethingWithNodes
will cause a crash trying to access already freed memory.
Unfortunately, C++ has no way to know if a raw pointer is valid or not. Your first instinct here might be to make the CCNode
a Ref
:
static std::unordered_map<Ref<CCNode>, SomeData> REGISTERED_NODES {};
void addDataToNode(CCNode* node, SomeData const& data) {
REGISTERED_NODES.insert({ node, data });
}
void doSomethingWithNodes() {
for (auto& [node, data] : REGISTERED_NODES) {
// ...
}
}
Now you can be assured that REGISTERED_NODES
only contains valid pointers to CCNode
s. However, now we have a memory leak: if the user switches scenes, the only reference left to the CCNode
will be the one in REGISTERED_NODES
, which is probably undesirable, since we’d want the node to be removed from REGISTERED_NODES
too when the node is no longer visible.
A primitive solution to fixing this would be to check the node’s ref count, and if it’s 1 then we know only REGISTERED_NODES
has a reference to it:
static std::unordered_map<Ref<CCNode>, SomeData> REGISTERED_NODES {};
void freeUnusedNodes() {
for (auto& [node, _] : REGISTERED_NODES) {
if (node->retainCount() == 1) {
REGISTERED_NODES.erase(node);
}
}
}
void addDataToNode(CCNode* node, SomeData const& data) {
REGISTERED_NODES.insert({ node, data });
}
void doSomethingWithNodes() {
freeUnusedNodes();
for (auto& [node, data] : REGISTERED_NODES) {
// ...
}
}
However, this plan immediately falls apart once some other mod does the same, as the ref count is never reaching 1.
For this reason, Geode provides another class: WeakRef
, which is like Ref
but it doesn’t change the ref count:
static std::unordered_map<WeakRef<CCNode>, SomeData> REGISTERED_NODES {};
void addDataToNode(CCNode* node, SomeData const& data) {
REGISTERED_NODES.insert({ node, data });
}
void doSomethingWithNodes() {
for (auto& [node, data] : REGISTERED_NODES) {
// ...
}
}
The difference between Ref
and WeakRef
is that Ref
allows you to access the pointed object directly since it’s guaranteed to be valid. However, WeakRef
has no such guarantees since it doesn’t impact the ref count, so you need to first lock it to see if the object is still valid:
auto ref = WeakRef(obj);
// Later on
// Check if the reference still points to a valid object
if (auto obj = ref.lock()) {
}
lock
returns a Ref
, so you have guaranteed safe access to the object for as long as you have the Ref
. As WeakRef
does not increment the reference count, if something frees the object beforehand, lock
will return a null Ref
.
Now, WeakRef
still won’t automatically remove itself from your maps, so you do still have to manually clear invalid pointers:
static std::unordered_map<WeakRef<CCNode>, SomeData> REGISTERED_NODES {};
void freeUnusedNodes() {
for (auto& [node, _] : REGISTERED_NODES) {
if (node.lock() == nullptr) {
REGISTERED_NODES.erase(node);
}
}
}
void addDataToNode(CCNode* node, SomeData const& data) {
REGISTERED_NODES.insert({ node, data });
}
void doSomethingWithNodes() {
freeUnusedNodes();
for (auto& [node, data] : REGISTERED_NODES) {
// ...
}
}
However, now you can assured that this scheme won’t break if some other mod does the same.
⚠️ Note that a node that has weak references to it is only freed once one of those weak references triggers a recheck of the object. This is because WeakRef internally does actually increment the reference count, and uses the “check if reference count is 1” trick to know that only weak references exist to an object. This might seem counter-productive, but that scheme does work if Geode’s WeakRef system is the only one using it at a time, which it should be if modders follow guidelines properly.