2016年5月4日 星期三

0504 菜鳥學習Cocos2d-x 3.x——內存管理


注意宣告使用 create 不用增加new 範例: auto scene = Scene::create(); //error auto scene = new Scene::create();
http://www.jellythink.com/archives/776

菜鳥學習Cocos2d-x 3.x——內存管理

亙古不變的東西

到現在,內存已經非常便宜,但是也不是可以無限大的讓你去使用,特別是在移動端,那麼點內存,那麼多 APP要搶著用,搞不好,你佔的內存太多了,系統直接幹掉你的APP,所以說了,我們又要老生常談了——內存管理。總結COM開發的時候,分析過COM的 內存管理模式;總結Lua的時候,也分析了Lua的內存回收機制;前幾天,還專門寫了C++中的智能指針在內存使用方面的應用;可見,內存管理無論是語言 層面,還是類庫層面,都有嚴格的標準和實施,對於Cocos2d-x來說,也是如此。那麼在Cocos2d-x中,它是如何進行內存管理的呢?這篇文章, 我就來總結一下關於Cocos2d-x的內存管理方面的知識。讓你輕鬆度過面試官的五指關(面試Cocos2d-x時,100%會問到的問題啊,上點心 吧)。

初窺Cocos2d-x內存管理

對於探究內存管理這種比較抽象的東西,最簡單的方法就是通過代碼來研究,首先通過創建一個簡單的場景來看看Cocos2d-x在完成創建一個對象的時候,它都幹了些什麼。
創建一個Scene:
auto scene = Scene::create();
函數create是一個靜態函數,看看create函數的源碼:
Scene *Scene::create()
{
    Scene *ret = new Scene();
    if (ret && ret->init())
    {
        ret->autorelease();
        return ret;
    }
    else
    {
        CC_SAFE_DELETE(ret);
        return nullptr;
    }
}
現在就涉及到了Cocos2d-x的內存管理相關的知識了。在Cocos2d-x中,關於對象的創建與初始化都是使用的new和init函數搭配的方式,這種方式叫做二段式創建,由於C++中,構造函數沒有返回值,無法通過構造函數確定初始化的成功與失敗,所以在Cocos2d-x中就大行其道的使用了這種二段式創建的方式,用起來還不錯,以後在自己的項目中,也可以採用。
由於這種方式在Cocos2d-x中經常被使用,所以觸控那幫傢伙就搞了個宏:CREATE_FUNC。如果想讓我們的類也使用這種二段式創建的方式,只需要在我們的類中加入以下代碼:
CREATE_FUNC(classname);
同時,需要定義一個init函數,這就OK了。我們來看看這個宏:
#define CREATE_FUNC(__TYPE__) \
static __TYPE__* create() \
{ \
    __TYPE__ *pRet = new __TYPE__(); \
    if (pRet && pRet->init()) \
    { \
        pRet->autorelease(); \
        return pRet; \
    } \
    else \
    { \
        delete pRet; \
        pRet = NULL; \
        return NULL; \
    } \
}
話說這些東西也都是基礎的C++知識,沒有多少需要說的了,當你看到代碼中的ret->autorelease(),一臉茫然,是的,你已經看到了Cocos2d-x的內存管理的觸角了。
ret->autorelease()是什麼?當我使用create函數創建了場景以後,我並沒有去delete,這也沒有問題。問題就發生在這個autorelease的使用上。序幕說完了,讓我們真正的開始Cocos2d-x的內存管理吧。
在Cocos2d-x中,關於內存管理的類有:
  • Ref類;
  • AutoreleasePool類;
  • PoolManager類。
Ref類幾乎是Cocos2d-x中所有類的父類,它是Cocos2d-x中內存管理的最重要的一環;上面說的autorelease函數就Ref類的成員函數,Cocos2d-x中所有繼承自Ref的類,都可以使用Cocos2d-x的內存管理。
AutoreleasePool類用來管理自動釋放對象。
PoolManager用來管理所有的AutoreleasePool,這個類是使用單例模式實現的。
下面就通過對上述三個類的源碼進行分析,看看Cocos2d-x到底是如何進行內存管理的。

Ref類

先來看看Ref類的定義,以下是Ref類的頭文件定義:
class CC_DLL Ref
{
public:
    /**
    * 獲取對象的所有權
    * 增加對象的引用計數
    */
    void retain();

    /**
    * 立即釋放對象的所有權
    * 同時會減少對象的引用計數,當引用計數達到0時,直接銷毀這個對象
    */
    void release();

    /**
    * 自動釋放對象的所有權
    * 將對象添加到自動釋放池
    * 當在下一幀開始前,當前的自動釋放池會被回收掉,並且對自動釋放池中的所有對象
    * 執行一次release操作,當對象的引用計數為0時,對象會被釋放掉。
    */
    Ref* autorelease();

    /**
    * 獲得對象的當前引用計數
    * 當創建對象的時候,引用計數為1
    */
    unsigned int getReferenceCount() const;
};
對於release函數的實現,這裡需要特別總結一下,先看看它的實現:
void Ref::release()
{
    CCASSERT(_referenceCount > 0, "reference count should greater than 0");
    --_referenceCount;

    if (_referenceCount == 0)
    {
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
        auto poolManager = PoolManager::getInstance();

        if (!poolManager->getCurrentPool()->isClearing() && poolManager->isObjectInPools(this))
        {
            // 這裡是非常重要的一點,在我們使用Cocos2d-x中經常出錯的地方
            // 當引用計數為0,同時這個對象還存在於autorelease池中的時候,就會出現一個斷言錯誤
            // 可以想到,當這個對象引用計數為0時,就表示需要釋放掉,如果它還在autorelease池中,
            // 當在autorelease池中再次被釋放時,就會出現錯誤,這種錯誤是不瞭解Cocos2d-x內存管理的
            // 編程人員經常犯的錯誤。
            // 
            // 出現這個錯誤的原因在於new/retain和autorelease/release沒有對應使用引起的
            CCASSERT(false, "The reference shouldn't be 0 because it is still in autorelease pool.");
        }
#endif
        delete this;
    }
}
上面也說道了,對於new和autorelease需要匹配使用,retain和release也需 要匹配使用,否則就會出現斷言錯誤,或者內存洩露;在非Debug模式下,就可能直接閃退了。這就是為什麼我們在使用create函數的時候,new成功 以後,就順便調用了autorelease,將該對象放入到自動釋放池中;而當我們再次想獲取該對象並使用該對象的時候,需要使用retain再次獲得該 對象的所有權,當然了,在使用完成以後,你應該記得調用release去手動完成釋放工作,這是你的任務。例如以下代碼:
auto obj = Scene::create();
obj->autorelease(); // Error
這是錯誤的,在create中,在創建成功的情況下,已經將obj對象放到了autorelease pool中了;當你再次放入autorelease pool後,當銷毀autorelease pool以後,就會出現兩次銷毀一個對象的情況,出現程序的crash。再例如以下代碼也是錯誤的:
auto obj = Scene::create();
obj->release(); // Error
當使用create函數創建對象以後,obj沒有所有權,當再次調用release時,就會出現錯誤的對象釋放。而正確的做法應該如下:
auto obj = Scene::create(); // 這裡retain和release對應,release一個已經被autorelease過的對象(例如通過create函數構造的對象)必須先retain
obj->retain();
obj->release();
這引用計數,又讓我想起了COM中的AddRef和Release。

AutoreleasePool類

AutoreleasePool類是Ref類的友元類,先來看看Autorelease類的聲明。
class CC_DLL AutoreleasePool
{
public:
    /**
    * 不能在堆上創建AutoreleasePool對象,只能在棧上創建
    * 這就決定過了,當出了對應的作用域,AutoreleasePool對象就會被自動釋放,例如RAII技巧實現的
    */
    AutoreleasePool();

    /**
    * 創建一個帶有指定名字的autorelease pool對象
    * 對於調試來說,這個名字是非常有用的。
    */
    AutoreleasePool(const std::string &name);

    ~AutoreleasePool();

    /**
    * 向autorelease pool中添加一個ref對象
    * 同一個對象可以多次加入同一個自動釋放池中(貌似會觸發斷言錯誤)
    * 當自動釋放池被銷毀的時候,它會依次調用自動釋放池中對象的release()函數
    */
    void addObject(Ref *object);

    /**
    * 清理自動釋放池
    * 依次調用自動釋放池中對象的release()函數
    */
    void clear();

#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
    /**
    * 判斷當前是否正在執行自動釋放池的清理操作
    */
    bool isClearing() const { return _isClearing; };
#endif

    /**
    * 判斷自動釋放池是否包含指定的Ref對象
    */
    bool contains(Ref* object) const;

    /**
    * 打印autorelease pool中所有的對象
    */
    void dump();

private:
    /**
    * 所有的對象都是使用的std::vector來存放的
    */
    std::vector<Ref*> _managedObjectArray;
    std::string _name;

#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
    /**
    *  The flag for checking whether the pool is doing `clear` operation.
    */
    bool _isClearing;
#endif
};
對於AutoreleasePool類來說,它的實現很簡單,就是將簡單的將對象保存在一個std::vector中,在釋放這個AutoreleasePool的時候,對保存在std::vector中的對象依次調用對應的release函數,從而完成對象的自動釋放。

PoolManager類

這貨又是干什麼的?當我們在閱讀AutoreleasePool的源碼的時候,在它的構造函數中,你會發現如下代碼:
AutoreleasePool::AutoreleasePool()
: _name("")
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
, _isClearing(false)
#endif
{
    _managedObjectArray.reserve(150);
    PoolManager::getInstance()->push(this);
}
在AutoreleasePool的析構函數中,又有如下代碼:
AutoreleasePool::~AutoreleasePool()
{
    CCLOGINFO("deallocing AutoreleasePool: %p", this);
    clear();

    PoolManager::getInstance()->pop();
}
哦,原來,我們把AutoreleasePool對象又放到了PoolManager裡了;原 來,PoolManager類就是用來管理所有的AutoreleasePool的類,也是使用的單例模式來實現的。該PoolManger有一個存放 AutoreleasePool對象指針的stack,該stack是由std::vector實現的。需要注意的是,cocos2d-x的單例類都不是 線程安全的,跟內存管理緊密相關的PoolManager類也不例外,因此在多線程中使用cocos2d-x的接口需要特別注意內存管理的問題。關於更安 全的單例模式,感興趣的同學可以去閱讀這篇《C++設計模式——單例模式》。接下來,我們先看看PoolManager的頭文件定義。
class CC_DLL PoolManager
{
public:
    /**
    * 獲得單例
    */
    static PoolManager* getInstance();

    /**
    * 銷毀單例
    */
    static void destroyInstance();

    /**
    * 獲得當前的autorelease pool,在引擎中,至少會有一個autorelease pool
    * 在需要的時候,我們可以創建我們自己的release pool,然後將這個autorelease pool添加到PoolManager中
    */
    AutoreleasePool *getCurrentPool() const;

    /**
    * 判斷指定的對象是否在其中的一個autorelease pool中
    */
    bool isObjectInPools(Ref* obj) const;

    friend class AutoreleasePool;

private:
    PoolManager();
    ~PoolManager();

    /**
    * 將AutoreleasePool對象添加到PoolManager中
    */
    void push(AutoreleasePool *pool);

    /**
    * 從PoolManager中移除AutoreleasePool對象
    */
    void pop();

    static PoolManager* s_singleInstance;

    /**
    * 用來保存所有的AutoreleasePool對象
    */
    std::deque<AutoreleasePool*> _releasePoolStack;
    AutoreleasePool *_curReleasePool;
};
關於PoolManager中各個函數的實現也是非常簡單的,這裡不做累述,各位可以去閱讀Cocos2d-x的源碼。

問題來了

說了這麼多,代碼也列了這麼多,我們create一個對象以後,放到了 AutoreleasePool中去了,最終,在調用AutoreleasePool的clear函數的時候,會對AutoreleasePool管理的 所有對象依次調用release操作。啊哈!貌似哪裡不對,我一直都沒有說最終誰會調用這個clear函數啊?是的。看下面這段在導演類中的代碼,我想你 會明白的。
void DisplayLinkDirector::mainLoop()
{
    if (_purgeDirectorInNextLoop)
    {
        _purgeDirectorInNextLoop = false;
        purgeDirector();
    }
    else if (! _invalid)
    {
        drawScene();

        // release the objects
        PoolManager::getInstance()->getCurrentPool()->clear();
    }
}
上面的代碼說明的事實是:在圖像渲染的主循環中,如果當前的圖形對象是在當前幀,則調用顯示函數,並 調用AutoreleasePool::clear()減少這些對象的引用計數。mainLoop是每一幀都會自動調用的,所以下一幀時這些對象都被當前 的AutoreleasePool對象release了一次。這也是AutoreleasePool「自動」的來由。

總結

好了,總結的差不多了,對於Cocos2d-x中的內存管理總結的差不多了。對於Cocos2d-x 中的內存管理,我個人認為,請時刻關注著這個對象的引用計數,retain和release,new和autorelease需要匹配使用,防止不必要的 錯誤發生。總結了這麼多,還是那句話。
紙上得來終覺淺,絕知此事要躬行。
只有經過實際的使用,在經過代碼的洗練,才能更好的去掌握這些。在Cocos2d-x中,很多地方已 經進行了autorelease,或者retain了,我們就不必再次進行這些操作,比如create,再比如在調用addChild方法添加子節點時, 自動調用了retain。對應的通過removeChild,移除子節點時,自動調用了release。這些地方稍微不注意,就可能會讓你掉入「坑」中。 努力吧,夥計們。

沒有留言:

張貼留言

cocos2dx-lua 建立滑鼠監聽

重要關鍵字  EVENT_MOUSE_SCROLL addEventListenerWithSceneGraphPriority      if IsPc() then --建立滑鼠監聽         local listener = cc.EventListenerMouse...