前言: Cocos2dx采用的源于Objective-c语言的引用计数机制来进行内存管理。如果接触过objective-c语言,那么引用计数机制就好理解了。在使用C++语言编程的时候,用new运算符为对象分配一块内存,当该对象不需要被使用的时候,用delete运算符释放掉该对象占用的内存。这样看起来挺不错的,为什么Cocos2dx还要采用引用计数机制呢?这是因为在开发过程中我们很容易犯一些与指针有关的错误,比如该释放内存却忘记释放内存,造成内存泄漏;然后有时候我们释放了ptr指向的那块内存,却没有将ptr置空,在不经意之间有可能会再一次释放ptr指向的内存,造成一些内存错误。那么我们就希望有这么一种机制的出现,让我们不需要花费太多的精力去想在什么时候去delete对象,所以Cocos2dx就采用了引用计数机制,可以很好的避免这样一些问题。
简述:Cocos2dx中有一个专门负责内存管理的类——Ref(后面会介绍它的源码),大部分的类都会继承这个基类。例如CCLayer,CCSprite,CCNode,CCScene
这样继承Ref的对象就会有一个引用计数(就是一个数字),当我们用new运算符构造一个继承Ref的对象的时候,会将引用计数初始化为1,调用Ref中的retain()方法使引用计数增加1,调用release()方法使引用计数减1,当引用计数为0的时候就会用delete运算符干掉该对象。
Cocos2dx内存管理:
Sprite* sprite=Sprite::create("helloworld.png"); layer->addchild(sprite);上面创建了一个Sprite对象,然后把这个对象加入到layer中,sprite就显示出来了,我们也不再需要关心什么时候去释放sprite了,sprite对象的生命周期与layer生命周期关联起来了,当layer被释放的时候,sprite也会跟着被释放。那么这是为什么呢?下面我们跟进到Sprite::create("")源码中去看看究竟。
Sprite* Sprite::create(const std::string& filename) { Sprite *sprite = new (std::nothrow) Sprite(); if (sprite && sprite->initWithFile(filename)) { sprite->autorelease(); return sprite; } CC_SAFE_DELETE(sprite); return nullptr; }我们可以从上面源码中看出来,首先new了一个Sprite对象,然后对该对象作了一个sprite->autorelease()操作,从命名上可以看出sprite会被自动释放。那么为什么sprite对象调用了autorelease方法之后就可以自动管理内存了呢?这是因为Sprite类继承了Ref类(绝大多数类都继承了Ref),而这个autorelease方法就是Ref中的成员方法。继续跟进到Ref源码中。
class Ref { public: /** * 引用计数加1 */ void retain(); /** * 引用计数减1,减完之后如果引用计数为0,则delete ptr */ void release(); /** * 将对象加入到autoReleasePool中,在每一帧的最后会对当前autoReleasePool中的每一个对象调用release()方法 */ Ref* autorelease(); /** * get方法,获取引用计数 */ unsigned int getReferenceCount() const; protected: /** * 构造函数,将构造函数属性置为protected,Ref无法被实例化,必须被继承才能起作用。 */ Ref(); public: virtual ~Ref(); protected: // 引用计数 unsigned int _referenceCount; friend class AutoreleasePool; };Ref类就是用来管理引用计数的,没有其他功能。一个继承自Ref类的对象调用retain()方法,引用计数加1;调用release()方法,引用计数减1;但是调用autoRelease()方法只是将指向该对象的指针加入到一个自动释放池中,并不会改变该对象的引用计数。
看完上述源码之后,我们大概就明白一些了,当new一个Sprite对象的时候,该对象的引用计数会被初始化为1,然后调用autoRelease()方法将对象加入到自动释放池中就ok了。再思考一个问题:当我们Sprite* sprite=Sprite::create("helloworld.png")创建一个Sprite对象,然后什么也不做,这个时候sprite的引用计数始终是1吗?该对象释放的时机在哪?事实上当sprite调用了autoRelease()方法后,引用计数会在每一帧的最后减1。下面接着分析。
void DisplayLinkDirector::mainLoop() { if (_purgeDirectorInNextLoop) { _purgeDirectorInNextLoop = false; purgeDirector(); } else if (_restartDirectorInNextLoop) { _restartDirectorInNextLoop = false; restartDirector(); } else if (! _invalid) { drawScene(); // release the objects PoolManager::getInstance()->getCurrentPool()->clear(); } }
mainLoop()执行一次就是一帧,每帧的结尾都会调用一个PoolManager::getInstance()->getCurrentPool()->clear()方法,接着跟进一下clear方法。
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 }从源码中可以看出,遍历一遍_managerdObjectArray,对每个obj调用release()方法,使得引用计数减1,减完之后如果引用计数为0,就delete掉该对象。在clear的方法最后还会清理一遍_managedObjectArray。
我们最后再来梳理一遍最先的例子:
帧开始:
1. Sprite* sprite=Sprite::create("helloworld.png"); 引用计数为1
2. layer->addChild(sprite); 引用计数为2 addChild()方法是Node中的成员方法,会增加sprite的引用计数。
帧结束: 引用计数为1 在帧结束之前有操作:PoolManager::getInstance()->getCurrentPool()->clear()。
帧结束的时候,sprite的引用计数为1,所以sprite会一直存在于内存中直到它的父类layer被释放。那么layer被释放的时候做了什么操作会使得sprite的引用计数减1呢?
在Node析构的时候,会遍历一遍Node中的_children数组,调用_children中的每一个对象的release方法,sprite的引用计数减1,最终被析构。