Пользователь
0,0
рейтинг
10 декабря 2013 в 21:47

Разработка → Cocos2d-x: несколько рекомендаций, как не допустить утечек памяти из песочницы tutorial

Cocos2d-x — это «движок», а точнее — набор классов, который сильно упрощает разработку графических приложений для операционных систем таких как iOS, Android, Windows phone, Windows, а также для HTML 5. В отличии от сocos2d-iphone, cocos2d-x предполагает разработку на C++, поэтому он такой универсальный. Те, кто пишет на C++ знают, что вся ответственность за выделение и освобождение памяти лежит на плечах программиста. Но разработчики cocos2d-x не плохо позаботились об этом и встроили в свой замечательный движок пул объектов, который предполагает использование смарт-поинтеров или, другими словами, умных указателей.Умный указатель содержит счетчик своих клиентов, когда счетчик равен нулю, память, выделенная под объект, очищается. В этой статье я покажу, как правильно создавать и удалять объекты в cocos2d-x, чтобы избежать утечек памяти. Все объекты классов cocos2d-x являются умными указателями. Эту функциональность они получили от своего родителя CCObject, который является начальным звеном в иерархии библиотеки классов движка.

Обычно программист, который хочет создать объект в хипе, пишет такой код:
CMyObject* pObject = new CMyObject(); // допустим, что в CMyObject — объявлен конструктор без параметров. 

В этом случае, в памяти создается CMyObject и указатель на него помещается в переменную pObject. После того как pObject больше не нужен надо вернуть память:
delete pObject;  

Обычно в рабочих проектах создается много объектов, часто не в одном модуле приложения, и надо следить, чтобы по окончанию все они были удалены, иначе мы получаем memory leak. Чтобы избежать такой неприятности, в cocos2d-x для создания объектов принято использовать открытые статические функции. Особенно когда программист расширяет функциональность классов движка, он должен придерживаться этого правила, чтобы избежать утечек памяти.
Рассмотрим пример создания спрайта в cocos2d-x:
...
CCSprite* pSprite = CCSprite::create(«spritename.png»); 
...

create — статическая функция — член класса CCSprite для cоздания объекта. Загляним в код этой функции:
CCSprite* CCSprite::create(const char *pszFileName)
{
    CCSprite *pobSprite = new CCSprite();
    if (pobSprite && pobSprite->initWithFile(pszFileName))
    {
        pobSprite->autorelease();
        return pobSprite;
    }
    CC_SAFE_DELETE(pobSprite);
    return NULL;
}

Сначала мы создаем объект, инициализируем его и с помощью кода:
pobSprite->autorelease();

добавляем в пул. Если инициализация прошла успешно, то нам вернется указатель на CCSprite, а если нет, то CC_SAFE_DELETE удалит pobSprite.
После выполнения этой функции, m_uReference у нашего pSprite равна еденице и жить ему осталось до выхода из локальной функции, так как пул обновляется на каждом такте движка. Здесь надо быть осторожными: если pSprite глобальная в переделах класса, то она будет указывать на мусор. Чтобы этого не произошло, нам надо воспользоваться созданным объектом. Например, добавить спрайт на слой:
pLayer->addChild(pSprite);
, или в массив
pArray->addObject(pSprite);
и т. п. При каждом добавлении pSprite его m_uReference  будет увеличиваться, не обязательно на 1. Это специфика реализации движка, внутри которого свои рабочие списки, массивы и т.д., они тоже влияют на m_uReference. А при каждом удалении pSprite, соответственно его m_uReference будет уменьшаться. Можно самим увеличивать или уменьшать m_uReference с помощью методов:
pSprite->retain(); // m_uReference +=1 
pStrite->release(); // m_uReference -=1

Осторожно пользуйтесь данными методами, каждому retain() должен соответствовать вызов release(). Иначе объект останется в памяти и произойдет memory leak.
Теперь поговорим о расширении классов. Допустим, нам нужен объект, который почти как CCSprite, но с дополнительным функционалом, скажем Bonus.
Приведу пример объявления производного класса из своего проекта:
class TABonus:public CCSprite
{
    TABonusType mType;
    TABonus(b2World* world, float pX, float pY,TABonusType type);
    virtual bool init();
public:
    float pBegX, pBegY;
    virtual ~TABonus();
    static TABonus* create(b2World* world, float pX, float pY,TABonusType type);
    inline TABonusType getType() {return mType;}
    void update(float pSecondElapsed);
};

Обратите внимание, конструктор объявлен в закрытой секции — это значит, что непосредственное создание объекта запрещено. Для этого в классе объявлена статическая функция create.
Посмотрим код функции:
TABonus* TABonus::create(b2World* world, float pX, float pY,TABonusType type)
{
   TABonus* pRet = new TABonus(world,pX,pY,type);
   if (pRet->init())
   {
     pRet->autorelease();
     return pRet;
   }
   CC_SAFE_DELETE(pRet);
   return NULL;
}

Он подобен рассмотреному ранее. Теперь, для создания TABonus, надо использовать следующий код:
TABonus* pBonus = TABonus::create(world,pX,pY, btCoins);// pBonus ->m_uReference == 0
pLayer->addChild(pBonus); //pBonus->m_uReference>0


Вывод:

— Постарайтесь избегать создания объектов напрямую через оператор new, для этого используйте статические функции. Также при наследовании у производного класса надо писать свои статические функции.
— Чтобы удалить объект, надо убрать его из всех контейнеров, куда он был добавлен, тогда m_uReference у него обнулится и память, выделенная под объект, вернется в кучу. Например, убрать pSprite со слоя:
pLayer->removeChild(pSprite, true);

или из массива:
pArray->removeObject(pSprite);

— Не используйте delete для объекта созданного в статической функции, ведь кроме Вас никто не знает, что объекта больше нет.
— Чтобы объект жил, надо увеличить его m_uReference путем добавления в контейнер, либо вызовом retain().

Играйте по правилам и выигрывайте.

Спасибо за внимание.
Анвар Эльдаров @elanserr
карма
2,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (15)

  • 0
    Еще можно сказать что есть встроенные макросы, например при наследовании, в объявлении класса можно пользоваться макросом CREATE_FUNC(CMyClass); ну и так же есть группа CC_SAFE_RELEASE, CC_SAFE_DELETE и так далее
  • –1
    Да, Вы совершенно правы. В cocos2d-x много полезных макросов, описаны они в файле CCPlatformMacros.h. И в объявлении класса можно написать строчку CREATE_FUNC(CMyClass). Этот макрос раскроется в статическую функцию для создания объекта CMyClass. Да что там говорить, смотрите сами:
    /**
     * define a create function for a specific type, such as CCLayer
     * @__TYPE__ class type to add create(), such as CCLayer
     */
    #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; \
        } \
    }
    
  • +2
    Почему не используют что-нибудь наподобие boost::shared_ptr или intrusive_ptr?
    • 0
      или std::shared_ptr ведь уже вполне можно использовать c++11
      • 0
        Третья версия как раз переезжает на канонические умные указатели. Ну и вообще, они собираются сделать Кокос ближе к С++-разработчикам, отходя от наследия Objective-C.
        • +3
          Вторая версия Cocos2d-x это не с++. Это все тот же Objective-C, но заточенный под с++ компилятор. В контексте современного с++ движок вообще спроектирован неправильно. Такая реализация, в принципе, не может быть сколько-нибудь эффективной. Да и радовать не может, постоянно приходится пилить какие-то костыли для решения тривиальных задач. То, что есть сейчас — это тяжелое бремя совместимости с Cocos2d. Надеюсь в 3-й версии забьют на совместимость и сделают полноценный и правильный с++ движок. Но это уже будет не Cocos2d…
    • 0
      Извиняюсь
    • 0
      По той же причине по которой не используют в других универсальных библиотеках. Нужен всего лишь умный указатель и тащить за собой целый буст и заставлять пользователей его собирать это не вариант. С появлением его в стл упростит задачу, но это произойдет когда большинство компиляторов его предоставят.
  • НЛО прилетело и опубликовало эту надпись здесь
    • –1
      Насчёт bad_alloc я согласен, но стандартный оператор new должен вернуть NULL в случае неудачи. Ведь в нём используется malloc, который в случае неудачи возвращает NULL. Я всегда так думал. Про конструктор… а если я хочу создать объект не в пуле? Мне еще один конструктор писать?
      Но суть не в этом. В cocos2d-x код не идеален и иногда приходится копаться в коде и исправлять баги. Но что в этой жизни совершенно?

    • 0
      Действительно, new не вернет NULL и будет исключение bad_alloc.
      stackoverflow.com/questions/550451/will-new-return-null-in-any-case
    • 0
      new (std::nothrow) T
      Возвращает nullptr в случае неудачи, обычный кидает std::bad_alloc, тут и спорить не о чем.
      Нигде не сказано, что new обязательно использует malloc внутри.
      Желающим могу дать ссылку на ISO C++.
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Спасибо за информацию, постараюсь на такие грабли не наступать. А можете привести пример, где надо менять сцены местами? Просто не приходилось, интересно узнать.
      • НЛО прилетело и опубликовало эту надпись здесь

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.