若是 Cocos2d-x 內存管理淺說 作爲初步認識,而 Cocos2d-x 內存管理的一種實現作爲進階使用,那麼本文將詳細的分析一下 Cocos2d-x 的內存管理的設計實現和原理。知其然,知其因此然 ~或者說:嗯,它這麼作,必定是有緣由的,體會設計者的用意,感同身受,若是是你,將會如何設計!~~架構
我以爲 最好的學習方式是以本身的語言組織,說與別人聽 ~ 這樣對本身:更容易發現平時容易忽略的問題,對別人:或多或少也有所助益!以學習爲目的,而別人的受益算是附帶的效果,這樣一個出發點 ~框架
由淺入深,總覽全局(或者由總體到局部)是我喜歡的出發點,或者思考角度,我不喜歡拘泥於細節的實現,由於那會加大考慮問題的複雜度,因此 把複雜的問題簡單化,是必然的過程。 那麼本文就說說 Cocos2d-x 的架構是如何設計以方便內存管理的。從理論到實踐 ~(固然是從我看問題的角度 :P,讀者若有異議,歡迎討論!文本使用 cocos2d-x 2.0.4 解說。)
引用計數的由來工具
cocos2d-x 的世界是基於 CCObject 類構建的,其中的每一個元素:層、場景、精靈等都是一個個 CCObject 的對象。因此 內存管理的本質就是管理一個個 CCObject。做爲一個 cocos2d 的 C++ 移植版本,在它以前有不少其它語言的 實現,從架構層次來講,這與語言的實現無關(好比 CCNode 的節點樹形關係,其它語言也能夠實現,若是是內存方便,C# 等更是無需考慮),但就從內存管理方面來講,參考了 OC (Objective-C) 的內存管理實現。學習
一個簡單的自動管理原則:CCObject 內部維護着一個引用計數,引用計數爲 0 就自動釋放 ~(若是麼有直接作如 delete 之類的操做)。那麼此時能夠預見,管理內存的實質就是管理這些 「引用計數」 了!使用 retain 和 release 方法對引用計數進行操做!
爲何要有自動釋放池 及其做用測試
咱們知道 cocos2d-x 使用了自動釋放池,自動管理對象,知其然!其因此然呢?爲何須要自動釋放池,它在整個框架之中又起着什麼樣的做用!在瞭解這一點以前,咱們須要 知道 CCObject 從建立之初,到最終銷燬,經歷了哪些過程。在此,一葉總結如下幾點:this
剛建立的對象,而 爲了保證在使用以前不會釋放(至少讓它存活一幀),因此自引用(也就是初始爲1) 爲了肯定是否 實際使用,因此須要在一個合適的時機,解除自身引用。 而這個什麼時候的時機正是在幀過分之時。 幀過分以後的對象,用則用矣,不用則棄! 因爲已經解除了自身引用,因此它的引用被使用者管理(通常而言,內部組成樹形結構的鏈式反應,如 CCNode)。 鏈式反應,也就是,若是釋放一個對象,也會釋放它所引用的對象。
上面是一個對象的大體流程,咱們將對象分爲兩個時期,一個是剛建立時期,自引用爲 1(若是爲 0 就會釋放對象,這是基本原則,因此要大於 0) 的時期,另外一個是使用時期。上面說到,爲了保證建立時期的對象不被銷燬,因此自引用(並無實際的使用)初始化爲 1,這就意味着咱們須要一個合適的時機,來解除這樣的自引用。spa
什麼時候?在幀過分之時!(這樣可保證當前幀能正確使用對象而沒有被銷燬。)怎麼樣釋放?因爲是自引用,咱們並不能經過其它方式訪問到它,因此就有了自動釋放池,咱們 變相的將「自引用」轉化「自動釋放池引用」,來標記一個 「建立時期的對象」。而後在幀過分之時,經過自動釋放池管理,統一釋放 「釋放池引用」,也就意味着,去除了「自身引用」。幀過分以後的對象,纔是真正的被使用者所管理。 下面咱們用代碼來解釋上述過程。debug
一般咱們使用 create(); 方法來建立一個自動管理的對象,而其內部實際操做以下:設計
// 初始化一個對象 static CCObject* create() { // new CCObject 對象 CCObject *pRet = new CCObject(); if (pRet && pRet->init()) { // 添加到自動釋放池 pRet->autorelease(); return pRet; } else { delete pRet; pRet = 0; return 0; } } // 咱們看到初始化的對象 自引用 m_uReference = 1 CCObject::CCObject(void) :m_uAutoReleaseCount(0) ,m_uReference(1) // when the object is created, the reference count of it is 1 ,m_nLuaID(0) { static unsigned int uObjectCount = 0; m_uID = ++uObjectCount; } // 標記爲自動釋放對象 CCObject* CCObject::autorelease(void) { // 添加到自動釋放池 CCPoolManager::sharedPoolManager()->addObject(this); return this; } // 繼續跟蹤 void CCPoolManager::addObject(CCObject* pObject) { getCurReleasePool()->addObject(pObject); } // 添加到自動釋放池的實際操做 void CCAutoreleasePool::addObject(CCObject* pObject) { // 內部是由一個 CCArray 維護自動釋放對象,而且此操做 會使引用 + 1 m_pManagedObjectArray->addObject(pObject); // 因爲初始化 引用爲 1,上面又有操做,因此引用至少爲 2 (可能還被其它所引用) CCAssert(pObject->m_uReference > 1, "reference count should be greater than 1"); ++(pObject->m_uAutoReleaseCount); // 變相的將自身引用轉化爲釋放池引用,因此減 1 pObject->release(); // no ref count, in this case autorelease pool added. }
上面即是經過 create() 方法建立對象的過程。文中說到,一個合適的時機,解除自身引用(也就是釋放池引用),那這又是在什麼時候進行的呢?程序的運行有一個主循環,控制着每一幀的操做,在每一幀畫面畫完之時會自動調用 CCPoolManager::sharedPoolManager()->pop(); 方法 ( 具體可參見文章Cocos2d-x 程序是如何開始運行與結束的 ,這裏咱們只要知道每一幀結束都會調用 pop() 方法),來自動清理 建立時期 的引用。如今咱們就來看看 pop() 的方法實現:code
void CCPoolManager::pop() { if (! m_pCurReleasePool) { return; } // 當前釋放池個數,pop 使用棧結構 int nCount = m_pReleasePoolStack->count(); // 釋放池當中存放的都是 建立時期 對象,此時解除釋放池引用 m_pCurReleasePool->clear(); // 當前釋放池,出棧,在這裏能夠看到判斷 nCount 是否大於 1,文後將會對此作具體說明 if(nCount > 1) { m_pReleasePoolStack->removeObjectAtIndex(nCount-1); m_pCurReleasePool = (CCAutoreleasePool*)m_pReleasePoolStack->objectAtIndex(nCount - 2); } /*m_pCurReleasePool = NULL;*/ } // 釋放池引用清理工做 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(); } }
到這裏,自動釋放池的做用也就完成了! 能夠說建立的對象在一幀 (但有特殊狀況,下一段說明) 以後就徹底脫離了 自動釋放池的控制,自動釋放池,對對象的管理也就在 建立時期起着做用!以後便交由使用者管理,釋放。
對」釋放池」的管理說明
咱們知道了釋放池管理着 建立時期 的對象,那麼對於釋放池自己是如何管理的?咱們知道對於釋放池,只須要有一個就已經可以知足咱們的需求了,而在 cocos2d-x 的設計中,使用了集合管理 一堆 釋放池。而在實際,它們又發揮了多大的用處?
// 釋放池管理接口 class CC_DLL CCPoolManager { // 釋放池對象集合 CCArray* m_pReleasePoolStack; // 當前操做釋放池 CCAutoreleasePool* m_pCurReleasePool; // 獲取當前釋放池 CCAutoreleasePool* getCurReleasePool(); public: CCPoolManager(); ~CCPoolManager(); void finalize(); void push(); void pop(); void removeObject(CCObject* pObject); // 添加一個 建立時期 對象 void addObject(CCObject* pObject); static CCPoolManager* sharedPoolManager(); static void purgePoolManager(); friend class CCAutoreleasePool; }; // 咱們從 addObject 開始看起,由上文能夠 addObject 是由 CCObject 的 autorelease 自動調用的 void CCPoolManager::addObject(CCObject* pObject) { getCurReleasePool()->addObject(pObject); } CCAutoreleasePool* CCPoolManager::getCurReleasePool() { // 若是當前釋放池爲空 if(!m_pCurReleasePool) { // 添加一個 push(); } CCAssert(m_pCurReleasePool, "current auto release pool should not be null"); return m_pCurReleasePool; } void CCPoolManager::push() { CCAutoreleasePool* pPool = new CCAutoreleasePool(); //ref = 1 m_pCurReleasePool = pPool; // 像集合添加一個新的釋放池 m_pReleasePoolStack->addObject(pPool); //ref = 2 pPool->release(); //ref = 1 }
從 addObject 開始分析,咱們知道在 addObject 以前,會首先判斷是否有當前的釋放池,若是沒有則建立,若是有,則直接使用,可想而知,在任何使用,任何狀況,經過 addObject 只須要建立一個釋放池便已經足夠使用了。事實上也是如此。再來看 pop 方法。
void CCPoolManager::pop() { if (! m_pCurReleasePool) { return; } int nCount = m_pReleasePoolStack->count(); // 清楚對 建立對象 的引用 m_pCurReleasePool->clear(); // 若是大於 1,這也保證着,在任什麼時候候,總有一個釋放池是可使用的 if(nCount > 1) { // 移除當前的釋放池 m_pReleasePoolStack->removeObjectAtIndex(nCount-1); // 將當前釋放池設定爲前一個釋放池,也就是 「出棧」的操做 m_pCurReleasePool = (CCAutoreleasePool*)m_pReleasePoolStack->objectAtIndex(nCount - 2); } /*m_pCurReleasePool = NULL;*/ }
看到這裏 我就不解了!什麼狀況下才能用到多個釋放池?按照設計的邏輯根本用不到。帶着這個疑問,我在 CCPoolManager::push() 方法以內添加了一句話打印(修改源代碼) CCLog("這裏要長長長的 **********"); ,而後從新編譯源文件,運行程序,發現實際的使用中,push 只被調用了兩次!咱們知道,經過 addObject 可能會自動調用 push() 一次,但也僅有一次,因此必定是哪裏手動調用了 push() 方法,纔會出現這種狀況,因此我繼續翻看源代碼,定位到了 bool CCDirector::init(void) 方法,在這裏進行了遊戲的全局初始化相關工做:
bool CCDirector::init(void) { CCLOG("cocos2d: %s", cocos2dVersion()); ... ... m_dOldAnimationInterval = m_dAnimationInterval = 1.0 / kDefaultFPS; m_pobScenesStack = new CCArray(); m_pobScenesStack->init(); ... ... m_fContentScaleFactor = 1.0f; ... ... // touchDispatcher m_pTouchDispatcher = new CCTouchDispatcher(); m_pTouchDispatcher->init(); // KeypadDispatcher m_pKeypadDispatcher = new CCKeypadDispatcher(); // Accelerometer m_pAccelerometer = new CCAccelerometer(); // 這裏手動調用了 push 方法,而在這以前的初始化過程當中,間接的使用了 CCObject 的 autorelease,已經觸發過一次 push 方法 CCPoolManager::sharedPoolManager()->push(); return true; }
因此咱們便可以看到 push 方法被調用了兩次,但其實若是咱們把這裏的手動調用放在方法的開始處,或者乾脆就不使用 CCPoolManager::sharedPoolManager()->push(); ,對程序也沒任何影響,這樣從頭至尾,只建立了一個自動釋放池,而這裏多建立的一個並無多大的用處。 或者用處不甚明顯,由於多建立一個釋放池是有其效果的,效果具體體如今哪裏,那就是 可使調用 push() 方法以前的對象,多存活一幀。,由於 pop 方法只對當前釋放池作了 clear 釋放。爲了方便起見,咱們使用 Cocos2d-x 內存管理淺說 裏面的方法觀察每一幀的狀況,看下面測試代碼:
// 關鍵代碼以下 CCLog("update index: %d", updateCount); // 在不一樣的幀作相關操做,以便觀察 if (updateCount == 1) { // 建立一個自動管理對象 layer = LSLayer::create(); // 建立一個新的自動釋放池 CCPoolManager::sharedPoolManager()->push(); // 再建立一個自動管理對象 sprite = LSSprite::create(); } else if (updateCount == 2) { } else if (updateCount == 3) { } CCLog("update index: %d end", updateCount); /// 打印代碼以下 cocos2d-x debug info [update index: 1] // 第一幀建立了兩個自動管理對象 cocos2d-x debug info [LSLayer().()] cocos2d-x debug info [LSSprite().()] cocos2d-x debug info [update index: 1 end] // 第一個過分幀只釋放了 sprite 對象 cocos2d-x debug info [LSSprite().~()] cocos2d-x debug info [update index: 2] cocos2d-x debug info [update index: 2 end] // 第二個過分幀釋放了 layer 對象 cocos2d-x debug info [LSLayer().~()] cocos2d-x debug info [update index: 3] cocos2d-x debug info [update index: 3 end]
能夠對比 sprite 和 layer 對象,兩個對象被放在了不一樣的自動釋放池之中。這就是 手動調用 push() 方法所能達到的效果,至於怎麼利用這個特性,幫助咱們完成特殊的功能?我想仍是不用了,這會增長咱們程序設計的 複雜度,在我看來,甚至想把,cocos2d-x 2.0.4 中那惟一一次調用的 push() 給刪了,以保持簡單(程序的第一次初始化「可能」會用到這個特性,不過目測是沒有多大關係的了 : P),在這裏只系統經過這個例子理解 自動釋放池是怎樣被管理的便可!
從自動釋放池管理 建立時期 對象,再到對釋放池的管理,咱們已經大概瞭解了一個對象的生命週期經歷了哪些! 下面簡單說說 使用時期 的對象管理。
樹形結構的鏈式反應
文中咱們知道了,自動釋放池的存在乎義,在於對象 建立時期 的處理,而僅僅理解了自動釋放池,對於咱們使用 cocos2d-x 不夠,遠遠不夠!自動釋放池只是解決對象初始化的問題,僅此而已,而要在整個使用過程當中,相對的自動化管理,那麼必須理解兩個概念,樹形結構 和 鏈式反應 (鏈式反應,不錯的說法,就像原子彈爆炸同樣,一傳十,十傳百 :P)
咱們當前運行這一個場景,場景初始化,添加了不少層,層裏面有其它的層或者精靈,而這些都是 CCNode 節點,以場景爲根,造成一個樹形結構,場景初始化以後(一幀以後),這些節點將徹底 依附 (內部經過 retain) 在這個樹形結構之上,全權交由樹來管理,當咱們 砍去一個樹枝,或者將樹 連根拔起,那麼在它之上的「子節點」也會跟着去除(內部經過 release),這即是鏈式反應。
Cocos2d-x 內存管理的一種實現,此文這種實現的本質既是 強化這種 鏈式反應,也是解決內存可能出錯的一個解決方案。以下(前文片斷,具體詳見前文):
// 方式一:那麼咱們的使用過程 LUser* lu = LUser::create(); lu->m_sSprite = CCSprite::create("a.png"); // 若是這裏不 retain 則之後就用不到了 lu->m_sSprite->retain(); // 方式二:使用方法 LUser* lu = LUser::create(); lu->m_sUserName = "一葉"; // 這裏的 sprite 會隨着 lu 的消亡而消亡,不用管釋放問題了 lu->setSprite(CCSprite::create("a.png"));
咱們看到方式二相比方式一的設計,它經過 setSprite 內部對 sprite 自己 retain,從而實現鏈式反應,而不是直接使用 lu->m_sSprite->retain();,這樣的好處是,我只要想着釋放 LUser,而不用考慮LUser 內部 sprite 的引用狀況就好了。如此才能把 cocos2d-x 內存的自動管理特性徹底發揮 ~
而要實現這樣管理的一個明顯特徵就是,隱藏 retain 和 release 操做 ~
稍做總結
關於 cocos2d-x 的內存管理從使用到原理,系列文章就到這裏了!(三篇也算系列 = =!) 由表象到內部的思考探索過程,其實在 淺說 當中對 cocos2d-x 的使用,便已經可以知曉內部細節設計之一二,透過現象看本質!三篇文章包含了,使用淺說(簡單的測試),一種防止內存泄漏的設計(增強鏈式反應),最後縱覽 cocos2d-x 的內存管理框架,對 CCObject 的生命週期作了簡單的說明,固然其中仍是隱藏一些細節的,好比管理都是用 CCArray 來管理,但咱們並無對 CCArray 作介紹,它是如何添加元素,如何引用等。在任什麼時候候咱們只針對一個問題進行思考,那咱們該把 CCArray 這樣的輔助工具類放在何處,若是你瞭解固然最好,不過不了解,那便 存疑 ,而後對相應的問題,分而治之 ~
存疑 能夠幫助一葉在某個時刻只針對某一個問題進行思考,從而使問題變的簡單。對文中所涉及的到的兩個類 CCPoolManager 和 CCAutoreleasePool 其中全部的方法並無面面俱到,固然有了總體思路,去 填充那些 小疑問將會變得簡單。