Cocos2d-x 3.0 内存管理机制
在C++中,动态内存分配是一把双刃剑,一方面,直接访问内存地址提高了应用程序的性能,与使用内存的灵活性;另一方面,由于程序没有正确地分配与释放造成的例如野指针,重复释放,内存泄漏等问题又严重影响着应用程序的稳定性。 人们尝试着不同的方案去避免这个问题,比较常用的如智能指针,自动垃圾回收等,这些要么影响了应用程序的性能,要么仍然需要依赖于开发者注意一些规则,要么给开发者带来了另外一些很丑陋的用法(实际上笔者很不喜欢智能指针)。因此,优秀的C++内存管理方案需要兼顾性能,易用性,所以到目前为止C++标准都没有给出真正的内存管理方案。 3.2.1 C++显式堆内存管理 C++使用new关键字在运行时给一个对象动态分配内存,并返回堆上内存的地址供应用程序访问,通过动态分配的内存需要在对象不再被使用时通过delete运算符将其内存归还给内存池。 显式的内存管理在性能上有一定优势,但是极其容易出错,事实上,我们总是不能通过人的思维去保证一个逻辑的正确。不能正确处理堆内存的分配与释放通常会导致以下一些问题: 3.2.2 C++11中的智能指针 根据用于分配内存的方法,C++有3种管理数据内存的方式:自动存储,静态存储和动态存储。其中静态存储用于存储一些整个应用程序执行期间都存在的静态变量,动态存储用于存储上一节讲述的通过new分配的内存单元。 而对于在函数内部定义的常规变量则使用自动存储空间,其对应的变量称为自动变量。自动变量在所属的函数被调用时自动产生,在该函数结束时消亡。实际上,自动变量是一个局部变量,其作用域为包含它的代码块。自动变量通常存储在栈上,这意味着进入代码块时,其中的变量将依次加入到栈中,而在离开该代码块时按相反的顺序释放这些变量。 由于自动变量通常不会导致内存问题,所以智能指针试图通过将一个动态分配的内存单元与一个自动变量关联,这个自动变量在离开代码块被自动释放的时候释放其内存单元,这使得程序员不再需要显式地调用delete就可以很好的管理动态分配的内存。 C++11使用三种不同的智能指针,unique_ptr,shared_ptr和weak_ptr。它们都是模板类型,我们可以通过如下的方式来使用它们: int main(){ shared_ptr up2(new int(22)); 每个智能指针都重载了*运算符,我们可以使用*up1这样的方式来访问所分配的堆内存。智能指针在析构或者调用reset成员的时候,都可能释放其所拥有的堆内存。三者之间的区别如下: 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等都支持垃圾回收机制。 垃圾回收主要有两种方式: 不管哪种方法,自动垃圾回收都可以使得内存管理更自然,更重要的是程序员几乎不用为此做出任何被约束的事情。 3.2.5 Cocos2d-x内存管理机制 然而垃圾回收机制通常需要语言级别的实现,C++目前并没有包含完整的垃圾回收机制。Cocos2d-x中的内存管理机制实际上是基于智能指针的一个变体。但是它同时使得程序员可以像垃圾回收机制那样不需要声明智能指针。 3.2.5.1 引用计数 Cocos2d-x中所有对象几乎都继承自Ref基类,Ref唯一的职责就是对对象进行引用计数管理: class CC_DLL Ref protected: protected: 当一个对象被使用new运算符分配内存时,其引用计数为1,调用retain()方法会增加其引用计数,调用release()则会减少其引用计数,release()方法会在其引用计数为0时自动调用delete运算符删除对象并释放内存。 除此之外,retain和release并没有做任何特别的事情,它只是帮助我们记录了一个对象被引用的次数,实际上在程序中很少直接单独使用retain 和release,因为最终最重要的还是要在设计的时候就明确它应该在哪个地方被释放,大多数引用的地方都只是一种弱引用关系,使用retain和release反而会增加复杂性。 auto node=new Node(); //引用计数为1 node->removeFromParent(); //引用计数为1 我们马上发现这不是我们想要的结果,如果忘记调用release就会导致内存泄漏。 3.2.5.2 autorelease声明一个指针为”智能指针” 回想前面讲述的智能指针,如果将一个动态分配的内存关联到一个自动变量,则当这个自动变量的生命周期结束的时候将会释放这块堆内存,从而使程序员不必担心其内存释放。我们是否可以借鉴类似的机制来避免手动释放UI元素呢? Cocos2d-x使用autorelease来声明一个对象指针为”智能指针”,但是这些”智能指针”并不单独关联到某个自动变量,而是全部被加入到一个AutoreleasePool中。在每一帧结束的时候对加入到AutoreleasePool中的对象进行清理,也即是说在Cocos2d-x中,一个“智能指针”的生命周期是从创建开始到当前帧结束。 Ref* Ref::autorelease() 如上的代码,Cocos2d-x通过autorelease方法将一个对象加入到AutoreleasePool中。 void DisplayLinkDirector::mainLoop() // release the objects 如上的代码,Cocos2d-x在每一帧结束的时候清理AutoreleasePool中的对象。 void AutoreleasePool::clear() 实际的实现机制是AutoreleasePool对池中每个对象执行一次release操作,假设该对象的引用计数为1,表示其从未被使用,则执行release之后引用计数为0,将会被释放。例如创建一个不被使用的Node: auto node=new Node(); //引用计数为1 可以预期,在该帧结束的时候node对象将会被自动释放。如果该对象被使用,则: auto node=new Node(); //引用计数为1 则在该帧结束的时候,AutoreleasePool对其执行一次release操作之后引用计数为1,该对象继承存在。当下次该节点被移除的时候引用计数为0,就会被自动释放。通过这样,就实现了Ref对象的自动内存管理。 然而,不管是C++11中的智能指针,还是Cocos2d-x中变体的“智能指针”,都需要程序员手动声明其是“智能”的: shared_ptr np1(new int()); //C++11声明智能指针 为了简化这种声明,Cocos2d-x使用静态的create方法()来返回一个”智能指针”对象,Cocos2d-x中大部分的类都可以通过create来返回一个“智能指针”,例如Node,Action等,同时我们自定义的UI元素也应该遵循这样的风格,来简化其声明: Node * Node::create(void) 3.2.5.3 AutoreleasePool队列 对于有些游戏对象而言,”一帧”的生命周期显然有些过长,假设一帧会调用100个方法,每个方法创建10个“智能指针”对象,并且这些对象只在每个方法作用域内被使用,则在该帧末尾的时候内存当中的最大峰值为1000个游戏对象所占用的内存,这样游戏的平均内存占用将会大大增加,而实际上每帧平均只需要占用10个对象的内存,假设这些方法是顺序执行的。 默认AutoreleasePool一帧被清理一次主要是用来清理UI元素的,由于UI元素大部分都是添加到UI树中,会一直占用内存的,这种情况下每帧清理并不会对内存占用有多大影响。 显然,对于自定义数据对象,我们需要能够自定义AutoreleasePool的生命周期。Cocos2d-x通过实现一个AutoreleasePool的队列来实现“智能指针”生命周期的自定义[引用5],并由PoolManager来管理这个AutoreleasePool队列: class CC_DLL PoolManager AutoreleasePool *getCurrentPool() const; friend class AutoreleasePool; private: void push(AutoreleasePool *pool); static PoolManager* s_singleInstance; std::deque _releasePoolStack; PoolManager初始和默认至少有一个AutoreleasePool,它主要用来存储前面讲述的Cocos2d-x中的UI元素对象。我们可以创建自己的AutoreleasePool对象,将其压入到队列尾端。但是如果我们使用new运算符来创建AutoreleasePool对象,则又需要手动释放,为了达到和智能指针使用自动变量来管理内存的效果,Cocos2d-x对AutoreleasePool的构造和析构函数进行了特殊处理,以使我们可以通过自动变量来管理内存释放: AutoreleasePool::AutoreleasePool() AutoreleasePool::AutoreleasePool(const std::string &name) PoolManager::getInstance()->pop(); AutoreleasePool在构造函数中将自身指针添加到PoolManager的AutoreleasePool队列中,并在析构函数中从队列中移除自己,由于前面讲述的Ref::autorelease()始终将自己添加到“当前AutoreleasePool”中,只要当前AutoreleasePool始终为队列尾端的元素,声明一个AutoreleasePool对象就可以影响之后的对象,直到该AutoreleasePool对象被移除队列。这样在程序中我们就可以这么使用: Class MyClass : public Ref void customAutoreleasePool() 在该方法开始执行时,声明一个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内存管理的注意事项: 原文:http://hielvis.com/2014/04/16/cocos2d-x-memory/ (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |