CCSprite * fish = new CCSprite;
CCLOG("After new: %d",fish->retainCount());
fish->init();
CCLOG("After init: %d",fish->retainCount());
fish->retain();
CCLOG("After retain: %d",fish->retainCount());
fish->release();
CCLOG("After release: %d",fish->retainCount());
fish->autorelease();
CCLOG("After autorelease: %d",fish->retainCount()); //实际操作了+1-1.
结果:
After new: 1
After init: 1
After retain: 2
After release: 1
After autorelease: 1
“HelloCpp.exe”(Win32): 已加载“C:\WINDOWS\SysWOW64\SogouTSF.ime”。无法查找或打开 PDB 文件。
class CC_DLL CCCopying
{
public:
virtual CCObject* copyWithZone(CCZone* pZone);
};
/**
* @js NA
*/
class CC_DLL CCObject : public CCCopying
{
public:
// object id, CCScriptSupport need public m_uID
unsigned int m_uID;
// Lua reference id
int m_nLuaID;
protected:
// count of references
unsigned int m_uReference;
// count of autorelease
unsigned int m_uAutoReleaseCount;
public:
CCObject(void);
/**
* @lua NA
*/
virtual ~CCObject(void);
void release(void);
void retain(void);
CCObject* autorelease(void);
CCObject* copy(void);
bool isSingleReference(void) const;
unsigned int retainCount(void) const;
virtual bool isEqual(const CCObject* pObject);
virtual void acceptVisitor(CCDataVisitor &visitor);
virtual void update(float dt) {CC_UNUSED_PARAM(dt);};
friend class CCAutoreleasePool;
};
如下:默认初始化reference为1,
CCObject::CCObject(void)
: m_nLuaID(0)
, m_uReference(1) // when the object is created, the reference count of it is 1
, m_uAutoReleaseCount(0) //
{
static unsigned int uObjectCount = 0;
m_uID = ++uObjectCount;
}
看看autorelease的源码就知道实现了+1和-1的操作:
CCObject* CCObject::autorelease(void)
{
CCPoolManager::sharedPoolManager()->addObject(this);
return this;
}
//<span style="font-family: Arial, Helvetica, sans-serif;">CCAutoreleasePool.cpp</span>
void CCPoolManager::addObject(CCObject* pObject)
{
getCurReleasePool()->addObject(pObject);
}
void CCAutoreleasePool::addObject(CCObject* pObject)
{
m_pManagedObjectArray->addObject(pObject); //赫然使用了CCArray。实现了+1的,随着3.2使用C++11语法后使用vector去管理就没有这么纠结了。
CCAssert(pObject->m_uReference > 1, "reference count should be greater than 1");
++(pObject->m_uAutoReleaseCount); //同时在当前的pool中增加管理
pObject->release(); // no ref count, in this case autorelease pool added. //必须-1,否则泄露。
}
void CCPoolManager::purgePoolManager()
{
CC_SAFE_DELETE(s_pPoolManager);
}
在2.2系列中,每个CCAutoreleasePool实际上就是一个CCArray,存储了一系列CCObject*,通过m_uAutoReleaseCount来计数。
void CCAutoreleasePool::clear()
{
if(m_pManagedObjectArray->count() > 0)
{
//CCAutoreleasePool* pReleasePool;
#ifdef _DEBUG
int nIndex = m_pManagedObjectArray->count() - 1;
#endif
CCObject* pObj = NULL;
CCARRAY_FOREACH_REVERSE(m_pManagedObjectArray, pObj)
{
if(!pObj)
break;
--(pObj->m_uAutoReleaseCount);
//(*it)->release();
//delete (*it);
#ifdef _DEBUG
nIndex--;
#endif
}
m_pManagedObjectArray->removeAllObjects();
}
}
回顾一下CCDiretor的初始化源码段:
// scheduler
m_pScheduler = new CCScheduler(); // 计时器
// action manager
m_pActionManager = new CCActionManager(); //动作管理器
m_pScheduler->scheduleUpdateForTarget(m_pActionManager, kCCPrioritySystem, false);
// touchDispatcher
m_pTouchDispatcher = new CCTouchDispatcher(); //触摸信号管理器
m_pTouchDispatcher->init();
// KeypadDispatcher
m_pKeypadDispatcher = new CCKeypadDispatcher(); //键盘信号管理器
// Accelerometer
m_pAccelerometer = new CCAccelerometer(); // 加速器管理器
// create autorelease pool
CCPoolManager::sharedPoolManager()->push(); //创建一个当前的pool并加入PoolManager中,还是一个CCArray,名为:m_pReleasePoolStack,实际上触控更换为vector是从2.0的版本左右就计划好了。2.x版本是个过渡,也就是说,了解了2.x,更具版本更新说明,3.x不存在代沟。
通告层NotificationNode使用说明
下面有个NotificationNode,实际上和Scene属于UI种类,因为是后绘制,会遮盖当前scene,用途在于loading或者提示这种弹窗,记得触摸屏蔽的相关设置
// Draw the Scene
void CCDirector::drawScene(void)
{
// calculate "global" dt
calculateDeltaTime();
//tick before glClear: issue #533
if (! m_bPaused)
{
m_pScheduler->update(m_fDeltaTime);
}
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
/* to avoid flickr, nextScene MUST be here: after tick and before draw.
XXX: Which bug is this one. It seems that it can't be reproduced with v0.9 */
if (m_pNextScene)
{
setNextScene();
}
kmGLPushMatrix();
// draw the scene
if (m_pRunningScene)
{
m_pRunningScene->visit();
}
// draw the notifications node
if (m_pNotificationNode)
{
m_pNotificationNode->visit();
}
if (m_bDisplayStats)
{
showStats();
}
kmGLPopMatrix();
m_uTotalFrames++;
// swap buffers
if (m_pobOpenGLView)
{
m_pobOpenGLView->swapBuffers();
}
if (m_bDisplayStats)
{
calculateMPF();
}
}
原以为会是在drawscene内调用,谁知不是。这是个题外话,无关紧要的。
回归正题:
在经过CCDisplayLinkDirector继承CCDirector,在mainLoop中调用poolManager,这个才是真正的cocos程序循环,程序入口的是win32或者ios的主程序循环:
简化版:
int CCApplication::run()
{
// Initialize instance and cocos2d.在这里构建scene、layer、sprite和建立渲染树、定时器schedule、消息派发表建立Dispatcher
if (!applicationDidFinishLaunching())
{
return 0;
}
CCEGLView* pMainWnd = CCEGLView::sharedOpenGLView();
pMainWnd->centerWindow();
ShowWindow(pMainWnd->getHWnd(), SW_SHOW);
while (1)
{
if (! PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
// Get current time tick.
QueryPerformanceCounter(&nNow);
// If it's the time to draw next frame, draw it, else sleep a while.
if (nNow.QuadPart - nLast.QuadPart > m_nAnimationInterval.QuadPart)
{
nLast.QuadPart = nNow.QuadPart;
CCDirector::sharedDirector()->mainLoop();
}
else
{
Sleep(0);
}
continue;
}
if (WM_QUIT == msg.message)
{
// Quit message loop.
break;
}
// Deal with windows message.
if (! m_hAccelTable || ! TranslateAccelerator(msg.hwnd, m_hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
void CCDisplayLinkDirector::mainLoop(void)
{
if (m_bPurgeDirecotorInNextLoop) /*虽然cocos2d允许多个director,准备兼容多窗口程序(实际上到目前3.2为止,cocos2d应用多为是单窗口,只有一个窗口句柄,支持多场景、层,支持C++11多线程的引擎 */
{//导演类清理自己
m_bPurgeDirecotorInNextLoop = false;
purgeDirector();
}
else if (! m_bInvalid)
{
drawScene();
// release the objects
CCPoolManager::sharedPoolManager()->pop();
}
}
实际上终于回到了我们要密切关注的CCObject内存回收机制了:
每次pop()都去调用当前CCAutoreleasePool的clear操作,因为原理在于Director将游戏世界切片为每一帧,并为每一帧设定绘制时间长度,当GPU和CPU在设备上完成上一帧的制作,会计算一个时间长度,长度不够时会占用当前帧的可用时长,当前帧绘制的必要时长于剩余帧时长时跳过当前帧的CPU执行和GPU绘制,这就是为什么掉帧和碰撞穿透的原因,而在clear操作中,代码如下:
void CCAutoreleasePool::clear()
{
if(m_pManagedObjectArray->count() > 0)
{
//CCAutoreleasePool* pReleasePool;
#ifdef _DEBUG
int nIndex = m_pManagedObjectArray->count() - 1;
#endif
CCObject* pObj = NULL;
CCARRAY_FOREACH_REVERSE(m_pManagedObjectArray, pObj)
{
if(!pObj)
break;
--(pObj->m_uAutoReleaseCount);
//(*it)->release();
//delete (*it);
#ifdef _DEBUG
nIndex--;
#endif
}
m_pManagedObjectArray->removeAllObjects();
}
}
将当前帧的数据清理掉,m_uAutoReleaseCount变成0,但是仍未真正释放内存。这时候内存管理机制就依靠m_uReference来辨别是否需要清理。所有只使用addChild而不retain的都会随着父节点被清理掉。一方面我们可以自己pushPool,另一方面可以使用挂载在m_pobScenesStack中当前scene下。poolManager并不管理m_pobScenesStack主线的节点,只管理掉落在当前pool中通过autorelease增加至pool的CCObject。
3.2 ref对比待续
ref 内部变化:变得清爽多了,除了增加了 CC_USE_MEM_LEAK_DETECTION 预编译模式下的内存跟踪:(这两个不是ref内部函数)
static void trackRef(Ref* ref);
static void untrackRef(Ref* ref);
外,减少了原先CCObject中的各种引用,只剩下:_referenceCount,无论lua还是js、c++,公用一个引用计数,C++分支真的往库或者大型3D多线程发展啦。只要坚持了过渡期,学好python、Linux的相关编译,手游纯3D的可行性很高。
下面才是新增的内存追踪显示函数:
#if CC_USE_MEM_LEAK_DETECTION
public:
static void printLeaks();
#endif
新的AutoreleasePool已经使用:std::vector<Ref*> _managedObjectArray; 标准C++作为容器,通过ref.autorelease()调用的代码中不在进行+-1啦,
void AutoreleasePool::addObject(Ref* object)
{
_managedObjectArray.push_back(object);
}
同时在mainLopp中变成了直接调用Autorelease的clear()操作:
void DisplayLinkDirector::mainLoop()
{
if (_purgeDirectorInNextLoop)
{
_purgeDirectorInNextLoop = false;
purgeDirector();
}
else if (! _invalid)
{
drawScene();
// release the objects
PoolManager::getInstance()->getCurrentPool()->clear();
}
}
3.2中
void Director::drawScene()
{
// calculate "global" dt
calculateDeltaTime();
// skip one flame when _deltaTime equal to zero.
if(_deltaTime < FLT_EPSILON)
{
return;
}
if (_openGLView)
{
_openGLView->pollInputEvents();
}
//tick before glClear: issue #533
if (! _paused)
{
_scheduler->update(_deltaTime);
_eventDispatcher->dispatchEvent(_eventAfterUpdate);
}
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
/* to avoid flickr, nextScene MUST be here: after tick and before draw.
XXX: Which bug is this one. It seems that it can't be reproduced with v0.9 */
if (_nextScene)
{
setNextScene();
}
pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
// draw the scene
if (_runningScene)
{
_runningScene->visit(_renderer, Mat4::IDENTITY, false);
_eventDispatcher->dispatchEvent(_eventAfterVisit);
}
// draw the notifications node
if (_notificationNode)
{
_notificationNode->visit(_renderer, Mat4::IDENTITY, false);
}
if (_displayStats)
{
showStats();
}
_renderer->render();
_eventDispatcher->dispatchEvent(_eventAfterDraw);
popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
_totalFrames++;
// swap buffers
if (_openGLView)
{
_openGLView->swapBuffers();
}
if (_displayStats)
{
calculateMPF();
}
}
通过调用setNextScene();来在每次绘制之前增加scene的引用计数,这样子在drawscene之后调用clear就能清楚了无用的内存ref对象:
void Director::setNextScene()
{
bool runningIsTransition = dynamic_cast<TransitionScene*>(_runningScene) != nullptr;
bool newIsTransition = dynamic_cast<TransitionScene*>(_nextScene) != nullptr;
// If it is not a transition, call onExit/cleanup
if (! newIsTransition)
{
if (_runningScene)
{
_runningScene->onExitTransitionDidStart();
_runningScene->onExit();
}
// issue #709. the root node (scene) should receive the cleanup message too
// otherwise it might be leaked.
if (_sendCleanupToScene && _runningScene)
{
_runningScene->cleanup();
}
}
if (_runningScene)
{
_runningScene->release();
}
_runningScene = _nextScene;
_nextScene->retain();
_nextScene = nullptr;
if ((! runningIsTransition) && _runningScene)
{
_runningScene->onEnter();
_runningScene->onEnterTransitionDidFinish();
}
}
同时看一下Node::addChild的操作,能解决我们的疑惑,使用了addChild和Autorelease两种内存管理的区别:
void Node::addChild(Node* child, int localZOrder, const std::string &name)
{
CCASSERT(child != nullptr, "Argument must be non-nil");
CCASSERT(child->_parent == nullptr, "child already added. It can't be added again");
addChildHelper(child, localZOrder, INVALID_TAG, name, false);
}
void Node::addChildHelper(Node* child, int localZOrder, int tag, const std::string &name, bool setTag)
{
if (_children.empty())
{
this->childrenAlloc();
}
this->insertChild(child, localZOrder);
if (setTag)
child->setTag(tag);
else
child->setName(name);
child->setParent(this);
child->setOrderOfArrival(s_globalOrderOfArrival++);
#if CC_USE_PHYSICS
// Recursive add children with which have physics body.
Scene* scene = this->getScene();
if (scene != nullptr && scene->getPhysicsWorld() != nullptr)
{
child->updatePhysicsBodyTransform(scene);
scene->addChildToPhysicsWorld(child);
}
#endif
if( _running )
{
child->onEnter();
// prevent onEnterTransitionDidFinish to be called twice when a node is added in onEnter
if (_isTransitionFinished) {
child->onEnterTransitionDidFinish();
}
}
if (_cascadeColorEnabled)
{
updateCascadeColor();
}
if (_cascadeOpacityEnabled)
{
updateCascadeOpacity();
}
}
这里要注意了,addChild中用到的vector不是std的vector,是CCVector,cocos2d重写的,
void pushBack(T object)
{
CCASSERT(object != nullptr, "The object should not be nullptr");
_data.push_back( object );
object->retain();
}
所以实际上我们看似使用AddChild交给cocos2d的渲染树帮助我们管理内存和启用AutoreleasePool形式管理内存是不相干,实际就相当于在以pool为载体的区域内建立了一批对象,根据需要去retian,或者挂载了渲染树上(每棵渲染树就是以scene为起点,addChild执行一次retian),每次渲染结束后都会清理一次内存池。所有在new之后,不调用Autorelease就要我们手动采取C++标准方式去管理内存;如果启用Autorelease的机制,因为调用Autorelease实在drawscene(cpu游戏主线和GPU渲染)之后,没有了之前存在可能申请了马上被清理的可能,所以不需要+-1,只需要在clear的时候判断是否为0。AddChild的区别在于挂载在渲染树上的节点_referenceCount初始为2,在执行一次clear之后就不在属于PoolManager的当中的AutoReleasePool,而是只属于渲染树啦。
渲染树的清理:
void Director::replaceScene(Scene *scene)
{
CCASSERT(_runningScene, "Use runWithScene: instead to start the director");
CCASSERT(scene != nullptr, "the scene should not be null");
if (scene == _nextScene)
return;
if (_nextScene)
{
if (_nextScene->isRunning())
{
_nextScene->onExit();
}
_nextScene->cleanup();
_nextScene = nullptr;
}
ssize_t index = _scenesStack.size();
_sendCleanupToScene = true;
_scenesStack.replace(index - 1, scene);
_nextScene = scene;
}
_nextScene->cleanup();
调用的是Node的cleanup,递归形式完成渲染树的清理。
提示: //利用静态函数实现每个pool创建自动添加至Manager,用的真妙!!C++只会越用越熟啊。
PoolManager* PoolManager::getInstance()
{
if (s_singleInstance == nullptr)
{
s_singleInstance = new PoolManager();
// Add the first auto release pool
new AutoreleasePool("cocos2d autorelease pool");
}
return s_singleInstance;
}
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);
}
所以每次执行完clear,Manager中除了渲染树和_referenceCount>1都会被清理掉;可是pool被清理之后_referenceCount>=1的内存就如渲染树般泄露了,这个时候就需要我用自己release(delete)了。为什么会出现这样子的情况呢,实际上源于早前兼容ObjectC的代码继承过来的管理机制,可能之后Pool和Autorelease的机制会被清理调用,只保存渲染树和手动retian、release。这和之后启用C++11 shared_ptr<typename>有关。因为3.2的pool实际上可有可无啦,完全可以让程序去实现了内存管理。
至于CCCopying被抽离成CloneAble,ref没有继承了clone接口,就不分析啦。可惜目前我仍然无法搭建git环境,我最怕搭建环境了,比如花了4天去配置cocos2dx 2.x 和quick的win安卓环境,3天安装mac虚拟机,后来发现3.x完全是方便至极。
下面是源码推荐的用法:
// Trigger an assert if the reference count is 0 but the Ref is still in autorelease pool.
// This happens when 'autorelease/release' were not used in pairs with 'new/retain'.
//
// Wrong usage (1):
//
// auto obj = Node::create(); // Ref = 1, but it's an autorelease Ref which means it was in the autorelease pool.
// obj->autorelease(); // Wrong: If you wish to invoke autorelease several times, you should retain `obj` first.
//
// Wrong usage (2):
//
// auto obj = Node::create();
// obj->release(); // Wrong: obj is an autorelease Ref, it will be released when clearing current pool.
//
// Correct usage (1):
//
// auto obj = Node::create();
// |- new Node(); // `new` is the pair of the `autorelease` of next line
// |- autorelease(); // The pair of `new Node`.
//
// obj->retain();
// obj->autorelease(); // This `autorelease` is the pair of `retain` of previous line.
//
// Correct usage (2):
//
// auto obj = Node::create();
// obj->retain();
// obj->release(); // This `release` is the pair of `retain` of previous line.