本文是阅读《我所理解的cocos2d-x》第二章——Cocos2d-x架构一瞥的2.1、2.2节两个小章节后的总结
(参考书籍《我所理解的cocos2d-x》,秦春林著)
(cocos2d-x版本为3.17)
在C++中,动态内存分配是一把双刃剑;一方面,直接访问内存地址提高了应用程序的性能及内存使用的灵活性;另一方面,由于程序没有正确的分配和释放而造成的野指针、重复释放、内存泄漏等问题又严重影响着应用程序的稳定性。
C++中使用new关键字在运行时给一个对象动态地分配内存,并返回堆上内存的地址供应用程序访问,被动态分配的内存需要在对象不再被使用时通过delete运算符将内存释放,归还给内存池。
如果不能够正确处理堆内存的分配与释放会导致以下问题。
1、野指针:指针指向的内存已经被释放
2、重复释放:重复释放已经释放的内存单元,或者释放一个野指针(也是重复释放)都会导致C++运行时错误。
3、内存泄漏:分配使用的内存单元没有被释放,就会被一直占用。
根据分配内存的方法,C++中有3种管理内存的方式
1、自动存储:函数内部的常规变量,存储在栈上,自动分配,自动释放内存。
2、静态存储:用于存储一些静态变量
3、动态存储:通过new关键字动态分配的内存单元
C++11使用3种不同的智能指针管理内存
1、unique_ptr:指针不能与其他智能指针共享所指对象的内存。
2、shared_ptr:多个shared_ptr指针可以共享同一分配的内存,它在实现上采用了引用计数。
3、weak_ptr:用来指向shared_ptr指针分配的对象内存,但不拥有该内存。(即指向的时候不会增加shared_ptr的引用计数),通常可用于验证shared_ptr指针的有效性。
cocos2dx为什么不使用上述3种C++智能指针?
1、智能指针有比较大的性能损失,shared_ptr为了保证线程安全,使用互斥锁保证所有线程访问时引用计数保持正确
2、虽然智能指针看起来很好用,但是使用的时候依旧需要显示声明。
cocos2dx的内存管理机制
1、引用计数(有点像是shared_ptr)
cocos2dx中的几乎所有对象都继承自Ref类。Ref类的主要指责也就是对对象的引用计数进行管理
class CC_DLL Ref
{
public:
void retain();//引用计数+1
void release();//引用计数-1
Ref* autorelease();//将对象交给内存池
unsigned int getReferenceCount() const;//获取当前的引用计数
protected:
Ref();
public:
virtual ~Ref();
protected:
/// count of references
unsigned int _referenceCount;//引用计数
friend class AutoreleasePool;
#if CC_ENABLE_SCRIPT_BINDING
public:
/// object id, ScriptSupport need public _ID
unsigned int _ID;
/// Lua reference id
int _luaID;
/// scriptObject, support for swift
void* _scriptObject;
/**
When true, it means that the object was already rooted.
*/
bool _rooted;
#endif
#if CC_REF_LEAK_DETECTION
public:
static void printLeaks();
#endif
};
首先我们看到数据成员 unsigned int _referenceCount;就是我们的引用计数。
看到Ref类的构造器
//可以看到,构造器初始化链表中,引用计数直接被赋值为1
Ref::Ref()
: _referenceCount(1) // when the Ref is created, the reference count of it is 1
#if CC_ENABLE_SCRIPT_BINDING
, _luaID (0)
, _scriptObject(nullptr)
, _rooted(false)
#endif
{
#if CC_ENABLE_SCRIPT_BINDING
static unsigned int uObjectCount = 0;
_ID = ++uObjectCount;
#endif
#if CC_REF_LEAK_DETECTION
trackRef(this);
#endif
}
转到retain函数的定义
//首先断言判断引用计数大于0,因为等于0的应该被释放掉内存
//断言判断之后就是引用计数+1
void Ref::retain()
{
CCASSERT(_referenceCount > 0, "reference count should be greater than 0");
++_referenceCount;
}
转到release函数的定义
//首先看到断言判断引用计数大于0
//引用计数-1
//减1之后判断引用计数是否为0,如果为0
//判断内存池是否正在清理以及对象是否在内存池中(如果在内存池中则报错,因为autoreleasePool的对象会遍历操作对象的引用计数
//判断之后,就可以将对象delete了,释放内存。
void Ref::release()
{
CCASSERT(_referenceCount > 0, "reference count should be greater than 0");
--_referenceCount;
if (_referenceCount == 0)
{
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
auto poolManager = PoolManager::getInstance();
if (!poolManager->getCurrentPool()->isClearing() && poolManager->isObjectInPools(this))
{
CCASSERT(false, "The reference shouldn't be 0 because it is still in autorelease pool.");
}
#endif
#if CC_ENABLE_SCRIPT_BINDING
ScriptEngineProtocol* pEngine = ScriptEngineManager::getInstance()->getScriptEngine();
if (pEngine != nullptr && pEngine->getScriptType() == kScriptTypeJavascript)
{
pEngine->removeObjectProxy(this);
}
#endif // CC_ENABLE_SCRIPT_BINDING
#if CC_REF_LEAK_DETECTION
untrackRef(this);
#endif
delete this;
}
}
我们看一下在工程开发过程中引用计数是如何变化的
auto node = new Node();//创建一个node对象,引用计数为1
addChild(node);//将node添加进当前对象中,引用计数为2
node->removeFromParent();//对象将自己从父节点中删除,此时的引用计数为1
node->release();//node对象将自己的引用计数再减一,变为零,内存被释放
我们看一下函数addChild和函数removeFromParent如何增加减少引用计数的
先来看addchild函数
//node类的addchild方法
void Node::addChild(Node* child, int localZOrder, const std::string &name)
{
CCASSERT(child != nullptr, "Argument must be non-nil");
CCASSERT(child->_parent == nullptr, "child already added. It can't be added again");
addChildHelper(child, localZOrder, INVALID_TAG, name, false);//看此处,我们跳进去
}
//——————————————————————————————————————————————————
//node类的addChildHelper
void Node::addChildHelper(Node* child, int localZOrder, int tag, const std::string &name, bool setTag)
{
auto assertNotSelfChild
( [ this, child ]() -> bool
{
for ( Node* parent( getParent() ); parent != nullptr;
parent = parent->getParent() )
if ( parent == child )
return false;
return true;
} );
(void)assertNotSelfChild;
CCASSERT( assertNotSelfChild(),
"A node cannot be the child of his own children" );
if (_children.empty())
{
this->childrenAlloc();
}
this->insertChild(child, localZOrder);//我们看到这里,跳进去
if (setTag)
child->setTag(tag);
else
child->setName(name);
child->setParent(this);
child->updateOrderOfArrival();
if( _running )
{
child->onEnter();
// prevent onEnterTransitionDidFinish to be called twice when a node is added in onEnter
if (_isTransitionFinished)
{
child->onEnterTransitionDidFinish();
}
}
if (_cascadeColorEnabled)
{
updateCascadeColor();
}
if (_cascadeOpacityEnabled)
{
updateCascadeOpacity();
}
}
//_________________________
//node类的insertChild
void Node::insertChild(Node* child, int z)
{
#if CC_ENABLE_GC_FOR_NATIVE_OBJECTS
auto sEngine = ScriptEngineManager::getInstance()->getScriptEngine();
if (sEngine)
{
sEngine->retainScriptObject(this, child);
}
#endif // CC_ENABLE_GC_FOR_NATIVE_OBJECTS
_transformUpdated = true;
_reorderChildDirty = true;
_children.pushBack(child);//我们看到这里!!!
child->_setLocalZOrder(z);
}
//Vector的push方法,因为_children是一个Vector
void pushBack(T object)
{
CCASSERT(object != nullptr, "The object should not be nullptr");
_data.push_back( object );
object->retain();//此处,引用计数+1
}
再来看removeFromParent函数
void Node::removeFromParent()
{
this->removeFromParentAndCleanup(true);
}
void Node::removeFromParentAndCleanup(bool cleanup)
{
if (_parent != nullptr)
{
_parent->removeChild(this,cleanup);//我们跳进这里
}
}
//——————————————————————————————————————————————————
void Node::removeChild(Node* child, bool cleanup /* = true */)
{
// explicit nil handling
if (_children.empty())
{
return;
}
ssize_t index = _children.getIndex(child);
if( index != CC_INVALID_INDEX )
this->detachChild( child, index, cleanup );//我们跳进这个函数
}
//———————————————————————————————————————————————————————
void Node::detachChild(Node *child, ssize_t childIndex, bool doCleanup)
{
// IMPORTANT:
// -1st do onExit
// -2nd cleanup
if (_running)
{
child->onExitTransitionDidStart();
child->onExit();
}
// If you don't do cleanup, the child's actions will not get removed and the
// its scheduledSelectors_ dict will not get released!
if (doCleanup)
{
child->cleanup();
}
#if CC_ENABLE_GC_FOR_NATIVE_OBJECTS
auto sEngine = ScriptEngineManager::getInstance()->getScriptEngine();
if (sEngine)
{
sEngine->releaseScriptObject(this, child);
}
#endif // CC_ENABLE_GC_FOR_NATIVE_OBJECTS
// set parent nil at the end
child->setParent(nullptr);
_children.erase(childIndex);//我们再跳进这个函数
}
//————————————————————————————————————————————————————————
iterator erase(ssize_t index)
{
CCASSERT(!_data.empty() && index >=0 && index < size(), "Invalid index!");
auto it = std::next( begin(), index );
(*it)->release();//在这个地方,引用计数减一
return _data.erase(it);
}
2、我们如何通过autorelease()方法声明一个“智能指针”
刚刚我们有看到,引用计数虽然帮助我们释放了内存,但是需要自己release,好麻烦
auto node = new Node();//创建一个node对象,引用计数为1
addChild(node);//将node添加进当前对象中,引用计数为2
node->removeFromParent();//对象将自己从父节点中删除,此时的引用计数为1
node->release();//node对象将自己的引用计数再减一,变为零,内存被释放
此时我们通过autorelease()来管理,cocos2dx使用autorelease()方法声明一个对象指针是智能指针,但是这些智能指针并不是单独关联某个自动变量,而是全部加进一个AutoReleasePool中,在每一帧结束的时候对autoReleasePool中的对象进行引用计数减1的处理。
我们看到Ref类的autorelease()方法
Ref* Ref::autorelease()
{
PoolManager::getInstance()->getCurrentPool()->addObject(this);//将当前对象加入当前的autoreleasePool()
return this;
}
//————————————————————————————————
void AutoreleasePool::addObject(Ref* object)
{
_managedObjectArray.push_back(object);//加进一个vector中
}
//在我们cocos2dx的主循环中
int Application::run()//太长了,删了很多代码=。=
{
while(!glview->windowShouldClose())
{
QueryPerformanceCounter(&nNow);
interval = nNow.QuadPart - nLast.QuadPart;
if (interval >= _animationInterval.QuadPart)
{
nLast.QuadPart = nNow.QuadPart;
director->mainLoop();//我们跳进这里
glview->pollEvents();
}
else
{
waitMS = (_animationInterval.QuadPart - interval) * 1000LL / freq.QuadPart - 1L;
if (waitMS > 1L)
Sleep(waitMS);
}
}
return 0;
}
//——————————————————————————————————————————
void Director::mainLoop()
{
if (_purgeDirectorInNextLoop)
{
_purgeDirectorInNextLoop = false;
purgeDirector();
}
else if (_restartDirectorInNextLoop)
{
_restartDirectorInNextLoop = false;
restartDirector();
}
else if (! _invalid)
{
drawScene();
// release the objects
PoolManager::getInstance()->getCurrentPool()->clear();//就是这里,调用了PoolManager的clear()方法
}
}
//————————————————————————————————
void AutoreleasePool::clear()
{
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
_isClearing = true;
#endif
std::vector<Ref*> releasings;
releasings.swap(_managedObjectArray);//此处使用vector的swap方法,将_managedObjectArray的对象全部移到releasings中,_managedObjectArray为空,不保留对象
for (const auto &obj : releasings)
{
obj->release();//然后对releasings中的对象都release(),引用计数减一,这就是刚刚所说的,一帧过后所有的内存池的对象引用计数减一,但是只会减去一次。
}
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
_isClearing = false;
#endif
}
3、AutoreleasePool队列
默认AutoreleasePool一帧被清理一次,主要是用来清理UI元素的,因为UI元素大部分是添加到UI树上,会一直占用内存。
那些不被添加到UI树上,又没有retain()的UI元素呢?一定要等到一帧之后才能被清理掉。假设我们需要循环调用一个函数,这个函数里面会创建10个UI元素,循环100次,不添加进UI树,只是临时使用。那么在被清理前就会占用极大内存。
对于这种有特殊要求的自定义的数据对象,我们要能够自定义AutoreleasePool的生命周期!
//因为对象在调用autorelease()方法的时候是自动添加进最新的AutorekeasPool中
Ref* Ref::autorelease()
{
PoolManager::getInstance()->getCurrentPool()->addObject(this);
return this;
}
//————————————————————————————————————
AutoreleasePool* PoolManager::getCurrentPool() const
{
return _releasePoolStack.back();//返回_releasePoolStack栈的最后(新)一个AutoreleasePool
}
//——————————————————————————————————————
std::vector<AutoreleasePool*> _releasePoolStack;//_releasePoolStack是一个vector,专门用来保存AutoreleasePool
PoolManager类的初始状态默认至少有一个AutoreleasePool,它主要用来存储前面讲述的cocos2d-x中的UI元素对象。
对于刚刚说的那种情况(创建大量的临时元素对象),我们可以创建自己的AutoreleasePool对象并加入_releasePoolStack的尾部。
AutoreleasePool在构造函数中将自身的指针添加进PoolManager的_releasePoolStack中。
AutoreleasePool::AutoreleasePool()
: _name("")
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
, _isClearing(false)
#endif
{
_managedObjectArray.reserve(150);
PoolManager::getInstance()->push(this);//此处将自身加进vector中
}
AutoreleasePool::~AutoreleasePool()
{
CCLOGINFO("deallocing AutoreleasePool: %p", this);
clear();//释放所有对象
PoolManager::getInstance()->pop();//此处将自己弹出来
}
前面讲到Ref::autorelease()方法始终将自己添加到当前的AutoreleasePool中,那么我们申明一个AutoreleasePool对象就可以影响之后的所有对象,直至这个AutoreleasePool生命周期结束。
class Myclass:public Ref
{
public:
static Myclass* create()
{
auto ref = new Myclass;
return ref->autorelease;//将自己加进当前的AutoreleasePool中
}
};
void customAutoreleasePool()
{
AutoreleasePool pool;//创建AutoreleasePool对象
auto ref1 = Myclass::create();//
auto ref2 = Myclass::create();//都将自己加进新创建的pool中了
}
//当customAutoreleasePool函数结束,pool声明周期结束。其析构函数会先调用clear()方法释放对象,再把自己弹出vector
//这就通过AutoreleasePool管理我们的对象
cocos中的智能指针
cocos2dx-3.x引入了智能指针RefPtr < T >。RefPtr< T >是基于RAII实现的。RAII(Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。
1、构造函数
RefPtr< T >需要依赖Ref的引用计数来管理内存,所有的类行T必须是Ref类型。Cocos2d-x通过静态转换static_const在编译时进行类型检查。
template <typename T> class RefPtr
{
public:
RefPtr(RefPtr<T> && other)
{
_ptr = other._ptr;
other._ptr = nullptr;//对于右值不会增加引用计数
}
RefPtr(T * ptr)
: _ptr(ptr)
{
CC_REF_PTR_SAFE_RETAIN(_ptr);
}
}
//————————————————————————————
#define CC_REF_PTR_SAFE_RETAIN(ptr)\
\
do\
{\
if (ptr)\
{\
const_cast<Ref*>(static_cast<const Ref*>(ptr))->retain();\//增加引用计数
}\
\
} while (0);
//————————————————————可以看到RefPtr变量和Ref指针是一种强引用关系,构造函数会对任何不是nullptr的Ref指针增加其引用计数,除非他是一个右值
2、赋值操作符
RefPtr< T >定义了一个对T的赋值操作符重载,。这样做可以使在对T进行转换的时候不用直接调用转换方法,从而对旧的资源进行释放。此外也可以使用nullptr让RefPtr成为一个空的智能指针。
与构造函数相似,对于任何左值变量的赋值,RefPtr都应该与该左值共享资源从而增加其引用计数,而对于右值,仍然保持转移而不是共享。与构造函数不同的是,赋值操作符除了会增加其资源的引用计数,还会释放对之前旧的资源的引用计数。
RefPtr<T> & operator = (T * other)
{
if (other != _ptr)
{
CC_REF_PTR_SAFE_RETAIN(other);//增加引用计数
CC_REF_PTR_SAFE_RELEASE(_ptr);//释放旧资源的引用计数
_ptr = other;//赋值新的资源引用计数
}
return *this;
}
#define CC_REF_PTR_SAFE_RETAIN(ptr)\
\
do\
{\
if (ptr)\
{\
const_cast<Ref*>(static_cast<const Ref*>(ptr))->retain();\
}\
\
} while (0);
#define CC_REF_PTR_SAFE_RELEASE(ptr)\
\
do\
{\
if (ptr)\
{\
const_cast<Ref*>(static_cast<const Ref*>(ptr))->release();\
}\
\
} while (0);
3、弱引用
赋值新的资源,但是不会增加引用计数
void weakAssign(const RefPtr<T> & other)
{
CC_REF_PTR_SAFE_RELEASE(_ptr);//释放之前的资源
_ptr = other._ptr;//赋值新的资源,但是不会增加引用计数
}
4、RefPtr< T >与容器
Vector< T >的pushBack方法接收T *指针,RefPtr会调用自己重载的operator T *方法,加入Vector中。
加入Vector中的元素内存也由Vector进行共享管理
//RefPtr的方法
operator T * () const { return _ptr; }
//cocos2dx 重写了Vector类,pushBack和popBack会分别retain和release
void pushBack(T object)
{
CCASSERT(object != nullptr, "The object should not be nullptr");
_data.push_back( object );
object->retain();
}
/** Push all elements of an existing Vector to the end of current Vector. */
void pushBack(const Vector<T>& other)
{
for(const auto &obj : other) {
_data.push_back(obj);
obj->retain();
}
}
void popBack()
{
CCASSERT(!_data.empty(), "no objects added");
auto last = _data.back();
_data.pop_back();
last->release();
}
5、RefPtr< T >的缺陷
Cocos2d-x中的智能指针存在一些缺陷
1、引用计数可以被RefPtr从外部控制
//1、引用计数可以被RefPtr从外部控制
auto str = new _String("Hello");
RefPtr<_String>ptr;
ptr.weakAssign(str);//弱引用不增加引用计数,
str.release();//引用计数减1,变为0
(*ptr)->getCString();//操作野指针
2、RefPtr提供的弱引用表现为一个强引用的行为,它仍然可以对其资源进行修改
RefPtr<_String>ptr1(new _String("Hello"));//引用计数为2
RefPtr<_String>ptr2;
ptr2.weakAssign(ptr1);//弱引用不增加引用计数,依然为2
ptr2.reset();//引用计数减1
ptr2.reset();//被释放
(*ptr2)->getCString();//错误,操作野指针。
在C++中,弱引用的std::weak_ptr被限制只能通过其lock成员来访问原std::shared_ptr变量,从而对资源内存进行操作,这样做能保证智能指针的有效性。
总结:
怎么样进行内存管理
1、所有的UI元素尽量交给autorelease来管理,游戏中的数据则可以用智能指针RefPtr来管理。
2、Ref的引用计数并不是线程安全的。多线程中,我们需要互斥锁保证安全。
3、对自定义的Node的子类,添加create方法,并使该方法返回一个autorelease对象
4、对自定义的数据类型,如果需要动态分配内存就继承Ref,然后使用智能指针RefPtr管理内存。
5、不要动态分配AutoreleasePool对象,要始终使用自动变量。
6、不要显示调用RefPtr的构造函数,始终使用隐式方式调用构造函数,因为显示的构造函数会导致同时执行构造函数和赋值操作符,这会造成一次不必要的临时智能指针变量的产生。