CCObject的分析:release、retain 基於2.2.3,增加3.2 ref對比,ccobjectretain
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,否則泄露。}
每個實現單例的類都應該提供清除操作:如下所示,以purge開頭。
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 Scenevoid 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(){ <span style="white-space:pre">// Initialize instance and cocos2d. 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); } }</span> }
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_DETECTIONpublic: 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.