在C++中,动态内存分配是一把双刃剑,一方面,直接访问内存地址提高了应用程序的
性能,与使用内存的灵活性;另一方面,由于程序没有正确地分配与释放造成的例如野指针,重复释放,内存泄漏等问题又严重影响着应用程序的稳定性。 人们尝试着不同的方案去避免这个问题,比较常用的如智能指针,
自动垃圾回收等,这些要么影响了应用程序的
性能,要么仍然需要依赖于开发者注意一些规则,要么给开发者带来了另外一些很丑陋的
用法(实际上笔者很不喜欢智能指针)。因此,优秀的C++内存管理方案需要兼顾
性能,易用性,所以到目前为止C++标准都没有给出真正的内存管理方案。 Cocos2d-x的内存管理机制实际上来源于Objective-C,这套机制几乎贯穿Cocos2d-x中所有的动态分配的对象。它使得管理动态分配到堆上的对象更简单,然而它独特的工作机制也使得一些开发者,尤其是不熟悉Objective-C的开发者对其造成一些”误解”。确保完整的理解,以及正确地使用Cocos2d-x的内存管理机制,是使用Cocos2d-x必须具备的基础准备工作。 3.2.1 C++显式堆内存管理 C++使用new关键字在运行时给
一个对象动态分配内存,并返回堆上内存的地址供应用程序访问,通过动态分配的内存需要在对象不再被使用时通过delete运算符将其内存归还给内存池。 显式的内存管理在
性能上有一定优势,但是极其容易出错,事实上,我们总是不能通过人的思维去保证
一个逻辑的正确。不能正确处理堆内存的分配与释放通常会导致以下一些问题: 野指针:指针指向的内存单元已经被释放,但是其他一些指针可能还指向它,这些内存可能已经被重新分配给其他对象,从而导致不可预测的结果。 重复释放:重复释放
一个已经被释放的内存单元,或者释放
一个野指针(也是重复释放)都会导致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()->ad
dobject(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 : _manage
dobjectArray) { obj->release(); } _manage
dobjectArray.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 { _manage
dobjectArray.reserve(150); PoolManager::getInstance()->push(this); } AutoreleasePool::AutoreleasePool(const std::string &name) : _name(name) #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0),_isClearing(false) #endif { _manage
dobjectArray.reserve(150); PoolManager::getInstance()->push(this); } AutoreleasePool::~AutoreleasePool() {
cclOGINFO(“deal
Locing 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对象,始终使用
自动变量。