深入理解 Cocos2d-x 內存管理

深入理解 Cocos2d-x 內存管理

JUN 4TH, 2013 | COMMENTS

[葉落歸根]: http://blog.leafsoar.com/archives/2013/06-04.html

如果 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 從創建之初,到最終銷燬,經歷了哪些過程。在此,一葉總結以下幾點:

  • 剛創建的對象,而 爲了保證在使用之前不會釋放(至少讓它存活一幀),所以自引用(也就是初始爲1)
  • 爲了確定是否 實際使用,所以需要在一個合適的時機,解除自身引用。
  • 而這個何時的時機正是在幀過度之時。
  • 幀過度之後的對象,用則用矣,不用則棄!
  • 由於已經解除了自身引用,所以它的引用被使用者管理(一般而言,內部組成樹形結構的鏈式反應,如 CCNode)。
  • 鏈式反應,也就是,如果釋放一個對象,也會釋放它所引用的對象。

上面是一個對象的大致流程,我們將對象分爲兩個時期,一個是剛創建時期,自引用爲 1(如果爲 0 就會釋放對象,這是基本原則,所以要大於 0) 的時期,另一個是使用時期。上面說到,爲了保證創建時期的對象不被銷燬,所以自引用(並沒有實際的使用)初始化爲 1,這就意味着我們需要一個合適的時機,來解除這樣的自引用。

何時?在幀過度之時!(這樣可保證當前幀能正確使用對象而沒有被銷燬。)怎麼樣釋放?由於是自引用,我們並不能通過其它方式訪問到它,所以就有了自動釋放池,我們 變相的將“自引用”轉化“自動釋放池引用”,來標記一個 “創建時期的對象”。然後在幀過度之時,通過自動釋放池管理,統一釋放 “釋放池引用”,也就意味着,去除了“自身引用”。幀過度之後的對象,纔是真正的被使用者所管理。 下面我們用代碼來解釋上述過程。

通常我們使用 create(); 方法來創建一個自動管理的對象,而其內部實際操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 初始化一個對象
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() 的方法實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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);

//         if(nCount > 1)
//         {
//             m_pCurReleasePool = m_pReleasePoolStack->objectAtIndex(nCount - 2);
//             return;
//         }
        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 的設計中,使用了集合管理 一堆 釋放池。而在實際,它們又發揮了多大的用處?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 釋放池管理接口
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 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void CCPoolManager::pop()
{
    if (! m_pCurReleasePool)
    {
        return;
    }

     int nCount = m_pReleasePoolStack->count();
  // 清楚對 創建對象 的引用
    m_pCurReleasePool->clear();

  // 如果大於 1,這也保證着,在任何時候,總有一個釋放池是可以使用的
      if(nCount > 1)
      {
        // 移除當前的釋放池
        m_pReleasePoolStack->removeObjectAtIndex(nCount-1);

//         if(nCount > 1)
//         {
//             m_pCurReleasePool = m_pReleasePoolStack->objectAtIndex(nCount - 2);
//             return;
//         }
      // 將當前釋放池設定爲前一個釋放池,也就是 “出棧”的操作
        m_pCurReleasePool = (CCAutoreleasePool*)m_pReleasePoolStack->objectAtIndex(nCount - 2);
    }

    /*m_pCurReleasePool = NULL;*/
}

看到這裏 我就不解了!什麼情況下才能用到多個釋放池?按照設計的邏輯根本用不到。帶着這個疑問,我在 CCPoolManager::push() 方法之內添加了一句話打印(修改源代碼) CCLog("這裏要長長長的 **********"); ,然後重新編譯源文件,運行程序,發現實際的使用中,push 只被調用了兩次!我們知道,通過 addObject 可能會自動調用 push() 一次,但也僅有一次,所以一定是哪裏手動調用了 push() 方法,纔會出現這種情況,所以我繼續翻看源代碼,定位到了 bool CCDirector::init(void) 方法,在這裏進行了遊戲的全局初始化相關工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 內存管理淺說 裏面的方法觀察每一幀的情況,看下面測試代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 關鍵代碼如下
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 內存管理的一種實現,此文這種實現的本質既是 強化這種 鏈式反應,也是解決內存可能出錯的一個解決方案。如下(前文片段,具體詳見前文):

1
2
3
4
5
6
7
8
9
10
11
// 方式一:那麼我們的使用過程
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 其中所有的方法並沒有面面俱到,當然有了整體思路,去 填充那些 小疑問將會變得簡單。

 Jun 4th, 2013  Cocos2d-x

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章