cocos2d-x 메모리 관리에 대한 간단한 정리

cocos2d-x는 cocos2d를 거의 그대로 포팅한 2d게임 엔진이다. 원작이 objc 기반인 cocos2d를 C++로 옮겼으니 완전히 똑같지는 않다. 특히 메모리 모델의 경우는 언어상의 차이로 다를수밖에 없다.

objc의 메모리 관리는 기본적으로 레퍼런스 카운팅 방식이다. 모든 objc의 클래스는 NSObject를 상속받는다. retain, release를 적절히 써서 레러런서 카운팅 쌍을 맞춰주면 적절히 메모리가 관리된다. autorelease를 사용하면 NSAutoreleasePool에 생성된 객체가 등록된다. 이후 NSAutoreleasePool이 해제되면 풀에 등록된 모든 객체에 대해서 release를 수행한다.

하지만 C++에는 기본적으로는 저런기능이 존재하지 않는다. new로 객체를 생성하고 delete로 객체를 해제한다. autorelase? 그딴건 니가 만들어야함ㅋ (물론 이제는 표준이된 std::shared_ptr, 일부컴파일러에서는 std::tr1::shared_ptr라는 이름으로 레퍼런스 카운팅이 되긴 한다. cocos2dx는 그런거 없이 자체 구현되어있으니까 일단 신경쓰지 않는다)

하지만 cocos2dx는 cocos2d를 거의 비슷하게 베끼려보니까 cocos2dx를 보면 CCObject라는 클래스로 objc에 있는 메모리 모델을 비슷하게 만들어놧다.

CCObject::CCObject(void)
{
    static unsigned int uObjectCount = 0;

    m_uID = ++uObjectCount;
    m_nLuaID = 0;

    // when the object is created, the refrence count of it is 1
    m_uReference = 1;		// <--- CCObject를 상속받은 클래스는 객체가 생성될때 카운터가 1
    m_bManaged = false;
}
CCObject::~CCObject(void)
{
    // if the object is managed, we should remove it
    // from pool manager
    if (m_bManaged)		//autorelease를 햇던 경우에 플래그 올라감
    {
        CCPoolManager::sharedPoolManager()->removeObject(this);	//풀에서 제거
    }
	//적절히 생략
}
void CCObject::release(void)
{
    CCAssert(m_uReference > 0, "reference count should greater than 0");
    --m_uReference;		//release는 레퍼런스 카운터 내리기

	//레퍼런스 카운터 0되면 클래스 자ㅋ폭ㅋ
	//그래서 release후에 delete를 수동으로 하면 망한다
    if (m_uReference == 0)	
    {
        delete this;
    }
}

void CCObject::retain(void)
{
    CCAssert(m_uReference > 0, "reference count should greater than 0");
    ++m_uReference;		//retain은 레퍼런스 카운터 올리기
}

CCObject* CCObject::autorelease(void)
{
	//autorelase를 하는 경우, 적절히 autorlease pool에 등록하고 플래그를 올린다
    CCPoolManager::sharedPoolManager()->addObject(this);
    m_bManaged = true;
    return this;
}

위와 같이 CCObject를 구현해서 objc의 retain,release,autorelase와 동일한 API를 유지할수 있도록 되어잇다. 다음으로 진짜 cocos2dx 사용 예제 소스에서 어떻게 메모리가 돌아가나 보자

수동으로 레퍼런스 카운팅을 하도록 짜면 다음과 같다. 레퍼런스 카운팅의 변화는 주석으로 달아놧다

bool SimpleLayer::init() {
	if(!CCLayer::init()) {
		return false;
	}
	CCSprite *sprite = new CCSprite();	//ref : 1
	sprite->initWithFile("asdf.png");	//레퍼런스 변화없음
	this->addChild(sprite);		//CCArray같은 cocoa의 자료구조를 베낀것에 집어넣으면 1증가. ref : 2
	sprite->release();	//수동으로 내려서 ref : 1
	//이후 이 레이어가 해제될때
	//children으로 등록되어잇는 sprite에 대해서 release가 적절히 수행되서 ref : 0, 누수없이 객체 삭제
	return true;
}

위의 코드는 평범한 objc코드를 c++로 바로 변환한거니까 별로 중요하지 않다. autorelease를 쓰는 예제는 다음과 같다. C++에는 원래 존재하지 않던 autoreleae를 적절히 만들어낸 cocos2dx의 신묘한 메모리 관리정책을 볼수잇다

bool SimpleLayer::init() {
	if(!CCLayer::init()) {
		return false;
	}
	CCSprite *sprite = CCSprite::spriteWithFile("asdf.png");	//ref:1, autorelease pool에 등록
	this->addChild(sprite);	//children등록되면서 +1, ref:1
	//이후 이 레이어가 해제될때
	//children으로 등록되어잇는 sprite에 대해서 release가 적절히 수행되서 ref : 1
	//ANG? ref=1이 남앗다고? 이건 그렇다면 언제 해제되는거지?
	return true;
}

예제의 코드만을 보면 ref=1이 남아있고 autorelase pool에 등록되어있는 상태이다. 그렇다면 레이어가 소멸햇지만 sprite는 해제되지 않고 남아잇는 상태라니 이게 무슨소리인가?

는 게임내부 소스를 까보면 신묘한 구현을 볼수있다.

void CCDisplayLinkDirector::mainLoop(void)
{
    if (m_bPurgeDirecotorInNextLoop)
    {
        m_bPurgeDirecotorInNextLoop = false;
        purgeDirector();
    }
    else if (! m_bInvalid)
     {
         drawScene();
     
         // release the objects
         CCPoolManager::sharedPoolManager()->pop();     //<--- 
     }
}
void CCPoolManager::pop()
{
    if (! m_pCurReleasePool)
    {
        return;
    }
	int nCount = m_pReleasePoolStack->count();
    m_pCurReleasePool->clear();	//<--
	.....
}
void CCAutoreleasePool::clear()
{
    if(m_pManagedObjectArray->count() > 0)
    {
        CCObject* pObj = NULL;
        CCARRAY_FOREACH_REVERSE(m_pManagedObjectArray, pObj)
        {
            if(!pObj)
                break;

            pObj->m_bManaged = false;
            //(*it)->release();
            //delete (*it);
       }
        m_pManagedObjectArray->removeAllObjects();
    }
}
void CCAutoreleasePool::addObject(CCObject* pObject)
{
    m_pManagedObjectArray->addObject(pObject);
    CCAssert(pObject->m_uReference > 1, "reference count should be greater than 1");
    pObject->release(); // no ref count, in this case autorelease pool added.
}

CCDirector::mainLoop는 매 프레임마다 호출되는 함수이다. 이 함수 안에는 autorelease pool을 해제하는 함수를 호출한다. CCPoolManager::pop을 거쳐서 CCAutoreleasePool::clear가 호출되서 공용 autorelease pool에 등록된 객체를 전부 해제한다.

autorelease pool을 clear하는 함수의 구현을 보면 모든 객체에 대해서 객체를 강제로 autorelease pool로 등록하지 않앗다고 플래그를 바꾸고 배열을 통째로 날린다. (배열을 비우면 배열안의 요소에 대해서 release가 호출된다)

autorelease pool에 객체를 등록하는 코드를 보면 레퍼런스 카운터를 원래상태로 유지하기 위해서 배열에 넣고 release를 수행한다

코드를 까봄으로써 알게된 추가사항을 다시 예제에 적용하면 다음과 같다

//객체 생성되서 ref=1
//autorelease pool에 등록해도 ref count는 변하지 않는다. ref=1
CCSprite *sprite = CCSprite::spriteWithFile("asdf.png");
this->addChild(sprite);	//children등록되면서 +1, ref:2
//이후 이 레이어가 해제될때
//children으로 등록되어잇는 sprite에 대해서 release가 적절히 수행되서 ref : 1

/*** 프레임 종료, mainLoop의 끝 ***/
//autorelase pool에 등록된 객체에 대해서 전부 relase를 호출해서 카운터 -1

//위에서 생성된 객체가 layer같은곳에 등록되어있어서 ref count=2인 상태이면 ref=1로 변함. 
//layer같은거에서 제거될때 레퍼런스 카운터가 1 줄어서 적절히 객체 해제됨

//autorelase로 객체를 생성하고 어디 등록하지 않고 냅둔경우(ref=1)
//autorelase가 적절히 release를 해제해서 카운터 1 줄어서 메모리 해제됨. 누수없음

좋은 메모리 관리 모델이다. 개발자는 initXXX을 써서 수동으로 관리하건 spriteXXX를 써서 자동으로 맡기건 어쨋든 잘 짜면 메모리 누수는 없다

하지만 이걸로 끝나지 않는다. 몇가지 문제점이 존재한다

  • retain, release는 락이 존재하지 않는다. 멀티 쓰레드로 가면 아마 사고칠거다
  • autorelase pool을 중첩해서 사용할수없다
    • mainLoop에서 처리하는 autorelase pool은 공용 autorelase pool 하나뿐이다

물론 위키에도 언급된 사항이다.
위의 개념과 엮여서 발생가능한 문제도 있는데 이는 위키를 적절히 참고하자. (하지만 나는 영어가 딸려서 결국 코드를 까봣지…-_- 차라리 C++이 더 쉬워)


comments powered by Disqus