内存管理
内存管理一直是一个不易处理的问题,开发者必须考虑分配回收的方式和时机,针对堆和栈做不同的优化处理,等等。内存管理的核心是动态分配的对象必须保证在使用完毕后有效地释放内存,即管理对象的生命周期。由于C++是一个较为底层的语言,其设计上不包含任何智能管理内存的机制。一个对象在使用完毕后必须被回收,然而在复杂的程序中,对象所有权在不同程序片段间传递或共享,使得确定回收的时机十分困难,因此内存管理成为了程序员十分头疼的问题。
另一方面,过于零散的对象分配回收可能导致堆中的内存碎片化,降低内存的使用效率。因此,我们需要一个合适的机制来缓解这个问题。
Boost库引入的智能指针(以及C++11引入的共享指针)从对象所有权传递的角度来解决内存管理问题。但是,在很多情况下,智能指针还是显得单薄而无力,因为实际开发中对象间的关系十分复杂,所有权传递的操作在开发过程中会变得冗杂不堪。于是,各种基于C++的第三方工具库和引擎往往都会实现自己的智能内存管理机制来解决内存管理的难题,试图将开发者从烦琐而晦涩的内存管理中解放出来。
主流的内存管理技术
目前,主要有两种实现智能管理内存的技术,一是引用计数,二是垃圾回收。
1)引用计数
它是一种很有效的机制,通过给每个对象维护一个引用计数器,记录该对象当前被引用的次数。当对象增加一次引用时,计数器加1;而对象失去一次引用时,计数器减1;当引用计数为0时,标志着该对象的生命周期结束,自动触发对象的回收释放。引用计数的重要规则是每一个程序片段必须负责任地维护引用计数,在需要维持对象生存的程序段的开始和结束分别增加和减少一次引用计数,这样我们就可以实现十分灵活的智能内存管理了。实际上,C++中的std::shared_ptr内部就是通过引用计数实现。
2)垃圾回收
它通过引入一种自动的内存回收器,试图将程序员从复杂的内存管理任务中完全解放出来。它会自动跟踪每一个对象的所有引用,以便找到所有正在使用的对象,然后释放其余不再需要的对象。垃圾回收器还可以压缩使用中的内存,以缩小堆所需要的工作空间。垃圾回收可以防止内存泄露,有效地使用可用内存。但是,垃圾回收器通常是作为一个单独的低级别的线程运行的,在不可预知的情况下对内存堆中已经死亡的或者长时间没有使用过的对象进行清除和回收,程序员不能手动指派垃圾回收器回收某个对象。回收机制包括分代复制垃圾回收、标记垃圾回收和增量垃圾回收等,一般垃圾回收机制应用在受托管的语言中(即运行在虚拟机中),如C#、Java以及一些脚本语言。
Cocos2d-x的内存管理
cocos2d-x中使用的是上面的引用计数来管理内存,但是又增加了一些自己的特色(自动回收池)。
cocos2d-x中通过Ref类(在2.x中是CCObject)来实现引用计数,所有需要实现内存自动回收的类都应该继承自Ref类。对象创建时引用计数为1,对象拷贝时引用计数加1,对象释放时引用计数减1,如果引用计数为0时释放对象内存。下面是Ref类的定义(为了简洁去掉了其中的断言语句,但不影响其代码完整性):
class CC_DLL Ref { public: // 将引用计数加1 void retain() { ++_referenceCount; } // 将引用技术减1,如果引用计数为0,释放当前对象 void release() { --_referenceCount; if (_referenceCount == 0) { delete this; } } // 将当前对象加入到当前的自动回收池中 Ref* autorelease() { PoolManager::getInstance()->getCurrentPool()->addObject(this); return this; } unsigned int getReferenceCount() const { return _referenceCount; } protected: Ref() : _referenceCount(1) // when the Ref is created, the reference count of it is 1 {} public: virtual ~Ref(); protected: /// count of references unsigned int _referenceCount; friend class AutoreleasePool; };
在cocos2d-x中创建对象通常有两种方式:
1)auto sprite = new Sprite; 2)auto sprite = Sprite::create(); Object *Object::create() { Object *obj = new Object; if (obj && obj->init()) { obj->autorelease(); } else { delete obj; obj = nullptr; } return obj; }
这两中方式的差异可以参见我另一篇博文“【cocos2d-x 3.x 学习笔记 02】对象创建方式讨论”。在cocos2d-x中提倡使用第二种方式,为了避免误用第一种方式,一般将构造函数设为 protected 或 private。
使用静态工厂方式 create() 创建对象,对象创建成功后会调用对象的autorelease()方法将对象加入到当前自动回收池中。场景每一帧绘制结束时会遍历当前自动回收池中的所有对象执行其release()方法,然后清空自动回收池中的对象。通过下面的函数调用顺序可以找到每一帧绘制结束时,清理自动回收池的地方:
// main.cpp return Application::getInstance()->run(); // Appdelegate.cpp int Application::run() { ... while(!glview->windowShouldClose()) { QueryPerformanceCounter(&nNow); if (nNow.QuadPart - nLast.QuadPart > _animationInterval.QuadPart) { nLast.QuadPart = nNow.QuadPart; // Enter loop director->mainLoop(); glview->pollEvents(); } else { Sleep(0); } } ... } // CCDirector.cpp void DisplayLinkDirector::mainLoop() { if (_purgeDirectorInNextLoop) { _purgeDirectorInNextLoop = false; purgeDirector(); } else if (! _invalid) { drawScene(); // release the objects PoolManager::getInstance()->getCurrentPool()->clear(); } } // CCAutoReleasePool.cpp void AutoreleasePool::clear() { for (const auto &obj : _managedObjectArray) { obj->release(); } _managedObjectArray.clear(); // std::vector<cocos2d::Ref *> 类型的数组 }
如果对象通过 create() 创建后没有使用,其默认引用计数为1,在场景第一帧绘制结束时会调用对象的release()方法将引用计数减1至0,由于引用计数为0释放对象所占用内存,同时将对象从自动回收池中清除掉。
如果对象通过 create() 创建后,通过addChild()加入某个父节点或直接调用对象的retain()函数,将导致对象的引用计数加1。在场景第一帧绘制结束时会调用对象的release()方法将引用计数减1,同时将对象从自动回收池中清除掉。此时对象的引用计数为1,但是对象已经不在自动回收池中,所以下一帧场景绘制清空自动回收池时对其没有影响,其占用的内存空间不会被自动释放,除非主动调用其release()函数将引用计数再次减1,从而使其引用计数变为0被释放(removeChild()或父节点被释放,这些操作内部实际会调用当前子节点的release()函数)。
所以,通过create()创建对象后,要么将其加入到其他节点中,要么调用对象的retian()函数,否则在其他地方(如事件回调函数)调用时,由于对象在一帧结束时被释放,会发生引用野指针抛出异常的情况。
最佳实践
如果想获得cocos2d-x的自动内存管理,就最好不要直接使用对象的 retain() 和 release()函数,如果确实需要那么也要成对的使用,调用一次retain()就必须在合适的时机和地方调用一次其release()函数。
参考资料:
[1]cocos2d-x 高级开发教程 2.3 节
[2]cocos2d-x 3.x 源代码