2、COCOS2D-X内存管理机制

时间:2023-12-31 20:14:08

在C++中。动态内存分配是一把双刃剑,一方面,直接訪问内存地址提高了应用程序的性能,与使用内存的灵活性。还有一方面。因为程序没有正确地分配与释放造成的比如野指针,反复释放,内存泄漏等问题又严重影响着应用程序的稳定性。





人们尝试着不同的方案去避免这个问题,比較经常使用的如智能指针,自己主动垃圾回收等,这些要么影响了应用程序的性能。要么仍然须要依赖于开发人员注意一些规则,要么给开发人员带来了另外一些非常丑陋的使用方法(实际上笔者非常不喜欢智能指针)。因此,优秀的C++内存管理方案须要兼顾性能。易用性,所以到眼下为止C++标准都没有给出真正的内存管理方案。





Cocos2d-x的内存管理机制实际上来源于Objective-C。这套机制差点儿贯穿Cocos2d-x中全部的动态分配的对象。它使得管理动态分配到堆上的对象更简单。然而它独特的工作机制也使得一些开发人员,尤其是不熟悉Objective-C的开发人员对其造成一些”误解”。确保完整的理解。以及正确地使用Cocos2d-x的内存管理机制。是使用Cocos2d-x必须具备的基础准备工作。





3.2.1 C++显式堆内存管理





C++使用newkeyword在执行时给一个对象动态分配内存。并返回堆上内存的地址供应用程序訪问。通过动态分配的内存须要在对象不再被使用时通过delete运算符将其内存归还给内存池。





显式的内存管理在性能上有一定优势,可是极其easy出错。其实。我们总是不能通过人的思维去保证一个逻辑的正确。不能正确处理堆内存的分配与释放一般会导致下面一些问题:

 野指针:指针指向的内存单元已经被释放,可是其它一些指针可能还指向它,这些内存可能已经被又一次分配给其它对象,从而导致不可预測的结果。

 反复释放:反复释放一个已经被释放的内存单元,或者释放一个野指针(也是反复释放)都会导致C++执行时错误。

 内存泄漏:不再被使用的内存单元假设不被释放就会一直占用内存单元。假设这些操作不断反复就会导致内存占用不断添加,在游戏中内存泄漏尤其严重,由于可能每一帧都在创建一个永远不会被回收的游戏对象。

3.2.2 C++11中的智能指针





依据用于分配内存的方法,C++有3种管理数据内存的方式:自己主动存储,静态存储和动态存储。当中静态存储用于存储一些整个应用程序运行期间都存在的静态变量,动态存储用于存储上一节讲述的通过new分配的内存单元。

而对于在函数内部定义的常规变量则使用自己主动存储空间,其相应的变量称为自己主动变量。自己主动变量在所属的函数被调用时自己主动产生,在该函数结束时消亡。实际上,自己主动变量是一个局部变量。其作用域为包括它的代码块。

自己主动变量通常存储在栈上,这意味着进入代码块时,当中的变量将依次增加到栈中。而在离开该代码块时按相反的顺序释放这些变量。

因为自己主动变量通常不会导致内存问题,所以智能指针试图通过将一个动态分配的内存单元与一个自己主动变量关联,这个自己主动变量在离开代码块被自己主动释放的时候释放其内存单元,这使得程序猿不再须要显式地调用delete就能够非常好的管理动态分配的内存。





C++11使用三种不同的智能指针,unique_ptr,shared_ptr和weak_ptr。它们都是模板类型,我们能够通过例如以下的方式来使用它们:





int main(){

unique_ptr up1(new int(11));

unique_ptr up11=up1; //编译报错





shared_ptr up2(new int(22));

weak_ptr up3=up2;

}





每一个智能指针都重载了*运算符,我们能够使用*up1这种方式来訪问所分配的堆内存。

智能指针在析构或者调用reset成员的时候,都可能释放其所拥有的堆内存。

三者之间的差别例如以下:

 unique_ptr不能与其它智能指针共享所指对象的内存。比如通过将up1赋值给up11将会导致编译错误。

但能够通过标准库的move函数来转移unique_ptr对对象的”拥有权”,一旦转移成功,原来的unique_ptr指针就失去了对象内存的全部权,再使用则会导致执行时错误。

 多个shared_ptr则能够共享同一堆分配对象的内存,它在实现上採用引用计数。一旦一个shared_ptr放弃了全部权(调用了reset成员)并不会影响其它智能指针对象。仅仅有全部引用计数归零的时候,才会真正释放所占有的堆内存的空间。

 weak_ptr能够用来指向shared_ptr分配的对象内存。可是却并不拥有该内存,我们能够使用其lock成员来訪问其指向内存的一个shared_ptr对象,当其所指向的内存无效时,返回指针空值(nullptr)。weak_ptr通常能够用来验证shared_ptr的有效性。





3.2.3 为什么不使用智能指针





看起来shared_ptr是一个完美的内存管理方案。然而实际上至少有两点原因使得Cocos2d-x不应该使用智能指针:





首先。智能指针有比較大的性能损失,Cocos2d-x论坛有过讨论是否使用智能指针的帖子[引用1]。shared_ptr为了保证线程安全,必须使用一定形式的相互排斥锁来保证全部线程訪问时其引用计数保持正确。这样的性能损失对于一般的使用是没有问题的,然而对于游戏这样的实时性很高的应用程序却是不可接受的,游戏须要一种更简单的内存管理模型。

其次,尽管智能指针能帮助程序猿进行有效的堆内存管理,可是它还是须要程序猿显式地声明智能指针,比如创建一个Node的代码须要这么写:





shared_ptr node(new Node());





另外。在我们须要引用的地方一般应该使用weak_ptr,否则在Node被移除的时候shared_ptr就会指向一个已释放的内存。导致执行时错误:





weak_ptr refNode=node;





这些额外的约束使得智能指针使用起来非常不自然,因此笔者特别讨厌智能指针,这样的用一种约束的方式来避免逻辑错误尽管可取,可是却并非一种优雅的方式,毕竟我们程序猿要天天面对代码。我们须要更自然的内存管理方式。就像语言自身的特性一样,我们甚至差点儿能够察觉不到其背后的机制。





3.2.4 垃圾回收机制





实际上,这种方案已经存在,这就是垃圾回收机制。垃圾回收的堆内存管理将之前使用过。现在不再使用或者没有不论什么指针再指向的内存空间称为“垃圾”,将这些“垃圾”收集起来以便再次利用的机制称为“垃圾回收”。

垃圾回收大约在1959年前后,由约翰 麦肯锡(John MaCarthy)为Lisp语言发明,在编程语言发展的过程中,垃圾回收的堆内存管理也得到了非常大的发展。现在流行的一些语言如Java。C#,Ruby。PHP,Perl等都支持垃圾回收机制。





垃圾回收主要有两种方式:

 基于引用计数:引用计数使用系统记录一个对象被引用的次数,当对象被引用的次数变为0时。该对象即被视作垃圾而被回收。这样的算法的长处是实现方式比較简单。

 基于跟踪处理:这样的方法是产生跟踪对象的关系图,然后进行垃圾回收。其算法是首先将程序中正在使用的对象视为“根对象”,从根对象開始查找它们所引用的堆空间。并在这些堆空间上做标记。

当标记结束之后全部未被标记的对象即被视作垃圾。在第二阶段会被清理。其第二阶段能够使用不同的方式进行清理,直接清理可能会产生大量垃圾碎片。还有一些方法对正在使用的对象进行移动或者拷贝。从而降低内存碎片的产生。





无论哪种方法,自己主动垃圾回收都能够使得内存管理更自然,更重要的是程序猿差点儿不用为此做出不论什么被约束的事情。





3.2.5 Cocos2d-x内存管理机制





然而垃圾回收机制通常须要语言级别的实现。C++眼下并没有包括完整的垃圾回收机制。Cocos2d-x中的内存管理机制实际上是基于智能指针的一个变体。

可是它同一时候使得程序猿能够像垃圾回收机制那样不须要声明智能指针。





3.2.5.1 引用计数





Cocos2d-x中全部对象差点儿都继承自Ref基类,Ref唯一的职责就是对对象进行引用计数管理:





class CC_DLL Ref

{

public:

void retain();

void release();

Ref* autorelease();

unsigned int getReferenceCount() const;





protected:

Ref();





protected:

/// count of references

unsigned int _referenceCount;

friend class AutoreleasePool;

};





当一个对象被使用new运算符分配内存时,其引用计数为1。调用retain()方法会添加其引用计数,调用release()则会降低其引用计数,release()方法会在其引用计数为0时自己主动调用delete运算符删除对象并释放内存。





除此之外。retain和release并没有做不论什么特别的事情,它仅仅是帮助我们记录了一个对象被引用的次数。实际上在程序中非常少直接单独使用retain 和release,由于终于最重要的还是要在设计的时候就明白它应该在哪个地方被释放。大多数引用的地方都仅仅是一种弱引用关系,使用retain和release反而会添加复杂性。

我们来看一下。在仅有引用计数的情况下我们应该如何管理UI元素:





auto node=new Node(); //引用计数为1

addChild(node); //引用计数为2

……





node->removeFromParent(); //引用计数为1

node->release(); //引用计数为0。对象被删除





我们立即发现这不是我们想要的结果,假设忘记调用release就会导致内存泄漏。

3.2.5.2 autorelease声明一个指针为”智能指针”





回忆前面讲述的智能指针。假设将一个动态分配的内存关联到一个自己主动变量,则当这个自己主动变量的生命周期结束的时候将会释放这块堆内存,从而使程序猿不必操心其内存释放。我们能否够借鉴类似的机制来避免手动释放UI元素呢?





Cocos2d-x使用autorelease来声明一个对象指针为”智能指针”,可是这些”智能指针”并不单独关联到某个自己主动变量,而是所有被增加到一个AutoreleasePool中。在每一帧结束的时候对增加到AutoreleasePool中的对象进行清理,也即是说在Cocos2d-x中。一个“智能指针”的生命周期是从创建開始到当前帧结束。

Ref* Ref::autorelease()

{

PoolManager::getInstance()->getCurrentPool()->addObject(this);

return this;

}





如上的代码,Cocos2d-x通过autorelease方法将一个对象增加到AutoreleasePool中。





void DisplayLinkDirector::mainLoop()

{

if (! _invalid)

{

drawScene();





// release the objects

PoolManager::getInstance()->getCurrentPool()->clear();

}

}





如上的代码。Cocos2d-x在每一帧结束的时候清理AutoreleasePool中的对象。





void AutoreleasePool::clear()

{

#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)

_isClearing = true;

#endif

for (const auto &obj : _managedObjectArray)

{

obj->release();

}

_managedObjectArray.clear();

#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)

_isClearing = false;

#endif

}





实际的实现机制是AutoreleasePool对池中每一个对象运行一次release操作,如果该对象的引用计数为1,表示其从未被使用,则运行release之后引用计数为0,将会被释放。

比如创建一个不被使用的Node:





auto node=new Node(); //引用计数为1

node->autorelease(); //增加”智能指针池”





能够预期。在该帧结束的时候node对象将会被自己主动释放。假设该对象被使用,则:





auto node=new Node(); //引用计数为1

node->autorelease(); //增加”智能指针池”

addChild(node); //引用计数为2





则在该帧结束的时候,AutoreleasePool对其运行一次release操作之后引用计数为1,该对象继承存在。当下次该节点被移除的时候引用计数为0。就会被自己主动释放。通过这样,就实现了Ref对象的自己主动内存管理。





然而。无论是C++11中的智能指针,还是Cocos2d-x中变体的“智能指针”,都须要程序猿手动声明其是“智能”的:





shared_ptr np1(new int()); //C++11声明智能指针

auto node=(new Node())->autorelease(); //Cocos2d-x中声明”智能指针”





为了简化这种声明,Cocos2d-x使用静态的create方法()来返回一个”智能指针”对象,Cocos2d-x中大部分的类都能够通过create来返回一个“智能指针”。比如Node,Action等,同一时候我们自己定义的UI元素也应该遵循这种风格。来简化其声明:





Node * Node::create(void)

{

Node * ret = new Node();

if (ret && ret->init()){

ret->autorelease();

}

else{

CC_SAFE_DELETE(ret);

}

return ret;

}





3.2.5.3 AutoreleasePool队列





对于有些游戏对象而言,”一帧”的生命周期显然有些过长。如果一帧会调用100个方法。每一个方法创建10个“智能指针”对象。而且这些对象仅仅在每一个方法作用域内被使用。则在该帧末尾的时候内存其中的最大峰值为1000个游戏对象所占用的内存。这样游戏的平均内存占用将会大大添加。而实际上每帧平均仅仅须要占用10个对象的内存,如果这些方法是顺序运行的。





默认AutoreleasePool一帧被清理一次主要是用来清理UI元素的。因为UI元素大部分都是加入到UI树中,会一直占用内存的,这样的情况下每帧清理并不会对内存占用有多大影响。

显然,对于自己定义数据对象。我们须要可以自己定义AutoreleasePool的生命周期。Cocos2d-x通过实现一个AutoreleasePool的队列来实现“智能指针”生命周期的自己定义[引用5],并由PoolManager来管理这个AutoreleasePool队列:





class CC_DLL PoolManager

{

public:

static PoolManager* getInstance();

static void destroyInstance();





AutoreleasePool *getCurrentPool() const;

bool isObjectInPools(Ref* obj) const;





friend class AutoreleasePool;





private:

PoolManager();

~PoolManager();





void push(AutoreleasePool *pool);

void pop();





static PoolManager* s_singleInstance;





std::deque _releasePoolStack;

AutoreleasePool *_curReleasePool;

};





PoolManager初始和默认至少有一个AutoreleasePool,它主要用来存储前面讲述的Cocos2d-x中的UI元素对象。我们能够创建自己的AutoreleasePool对象。将其压入到队列尾端。可是假设我们使用new运算符来创建AutoreleasePool对象,则又须要手动释放,为了达到和智能指针使用自己主动变量来管理内存的效果,Cocos2d-x对AutoreleasePool的构造和析构函数进行了特殊处理。以使我们能够通过自己主动变量来管理内存释放:





AutoreleasePool::AutoreleasePool()

: _name(“”)

#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)

, _isClearing(false)

#endif

{

_managedObjectArray.reserve(150);

PoolManager::getInstance()->push(this);

}





AutoreleasePool::AutoreleasePool(const std::string &name)

: _name(name)

#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)

, _isClearing(false)

#endif

{

_managedObjectArray.reserve(150);

PoolManager::getInstance()->push(this);

}





AutoreleasePool::~AutoreleasePool()

{

CCLOGINFO(“deallocing AutoreleasePool: %p”, this);

clear();





PoolManager::getInstance()->pop();

}





AutoreleasePool在构造函数中将自身指针加入到PoolManager的AutoreleasePool队列中,并在析构函数中从队列中移除自己,因为前面讲述的Ref::autorelease()始终将自己加入到“当前AutoreleasePool”中,仅仅要当前AutoreleasePool始终为队列尾端的元素,声明一个AutoreleasePool对象就能够影响之后的对象。直到该AutoreleasePool对象被移除队列。这样在程序中我们就能够这么使用:





Class MyClass : public Ref

{

static MyClass* create(){

auto ref=new MyClass();

return ref->autorelease();

}

}





void customAutoreleasePool()

{

AutoreleasePool pool;

auto ref1=MyClass::create();

auto ref2=MyClass::create();

}





在该方法開始运行时,声明一个AutoreleasePool类型的自己主动变量pool。其构造函数会将自身增加的PoolManager的AutoreleasePool队列尾端。接下来ref1和ref2都会被增加到pool池中。当该方法结束时。pool自己主动变量的生命周期结束。其析构函数将会释放对象,并从队列中移除自己。

这样我们就行通过自己定义AutoreleasePool的生命周期来控制Cocos2d-x中“智能指针”的生命周期。





3.2.5.4 总结





Cocos2d-x有一套性能高效且实现静止的内存管理机制,它本质上是一种“智能指针”的变体。它通过Ref::autorelease来声明一个“智能指针”,并通过将autorelease包装在create方法中,避免了程序猿对“智能指针”的声明。默认在一帧结束的时候AutoreleasePool会清理全部的“智能指针对象”。而且我们能够自己定义AutoreleasePool的作用域。





结合Cocos2d-x内存管理机制和特点,本节最后总结一些使用Cocos2d-x内存管理的注意事项:

1. Ref的引用计数并非线程安全的。在多线程中我们须要处理相互排斥锁来保证线程安全。在Objective-C中因为AutoreleasePool是语言级别系统实现的,每一个线程都有自己的AutoreleasePool队列。

2. 对于自己定义Node的子类。为该类加入create方法。该方法返回一个autorelease对象。

3. 对于自己主动义数据类型,假设须要动态分配内存的,继承自Ref。并加入create静态方法返回autorelease对象。

4. 仅仅在一个方法内部使用的Ref对象,使用自己定义的AutoreleasePool来即时清理内存占用。

5. 不要动态分配AutoreleasePool对象。始终使用自己主动变量。