cocos2d-xのリファレンスカウンタを理解してクラッシュやメモリリークを防ぐ


refarence

画像のはcocos2d-xのCCObjectの中身ですよ。
cocos2d-xはC++を使っているのですが、基本C++にはJavaやC#にあるようなガベージコレクションといった便利なものがなく、本来はインスタンス生成で確保したメモリは自分で解放しなければなりません。ただcocos2d-xの場合はObject-Cと同じようにリファレンスカウンタでメモリ管理をする仕組みが実装されており、おかげでそこまで意識しなくてもcocos2d-x側が自動で使わなくなったものを削除してくれるようになっています。

とはいえ、その仕組みをある程度理解しておかないと思わぬところでメモリが解放されてクラッシュしたり、メモリリークを起こしてしまったりしちゃうので、既に何度もクラッシュさせたりメモリリークを起こしちゃった自分の為にちょっと詳しくまとめておきます(´・ω・`)

あとどういう風に実装してるのかなっていうのも調べてみました。

cocos2dxのメモリ管理の仕組み

ccobject_life

わかりづらいですけど、こんな感じです。deleteの部分がdelete()になっちゃってるけど間違えただけなので気にしないで下さい(´・ω・`)

cocos2d-xでのメモリ管理はCCObject内にあるm_uReferenceという変数によって行われています。

仕組みは結構簡単で、CCObjectの生成時にm_uReferenceは1に初期化されます。
そしてretain()が呼び出されると+1されrelease()が呼び出されると-1されます。
release()時にm_uReferenceが0になったら、自ら命を絶ちます

m_uReferenceの値の増減

  • インスタンス生成時:1に初期化
  • retain()呼び出し:1増える
  • release()呼び出し:1減り、0になったら自殺

基本retain()、release()でほぼ全部管理されてる

cocos2d-x内では、ほぼ全てこのretain()、release()にてメモリが管理されており、cocos2dのクラスのあちこちで呼び出されています。
(そうじゃないものは従来のC++と同じように明示的にdeleteやfreeを行っています)
たとえばCCArray::addObject()した場合、内部でretain()が発行されます。逆にremoveObject()時にはrelease()が発行されます。CCDictionaryも同様です。
CCNodeでは子ノードをCCArrayで管理しているので、結果的にaddChild、removeChildでもretainとreleaseは行われています。

リファレンスカウンタがどこで増え、どこで減るかをコーディングする時に意識しておけばメモリリークなどの問題はほぼ発生しなくなります。

CCObjectはautorelease()呼び出しで初めて自動で削除される

CCObjectを継承するクラスをcreate()する際、その内部ではどれもautorelease()が呼び出されています。
全てのCCObjectは、このautorelease()メソッドを1度呼び出すことで初めて自動で削除されるようになり、どこかでretain()しないとすぐに削除されます。
たとえばメンバ変数にCCObjectを継承するクラスを使用する場合、retain()するかautorelease()をしないようにしないとすぐ解放されてクラッシュします。
(addChildやaddObjectした場合は内部でretain()がされるのでその必要はありません)

逆に言えば、このメソッドを呼び出さなかった場合、プログラマ側で明示的にrelease()しないとメモリリークを起こします。

  • autorelease()呼び出し:自動で削除されるようになる
  • 呼び出さない場合:明示的にrelease()する必要がある
  • 呼び出した場合:retain()しないとすぐ解放されクラッシュする

autoreleaseの仕組み

ちなみにいつのタイミングで解放されるのでしょうね?というのも調べてみました。
autorelease()を呼び出すと、CCObjectは自分をCCAutoreleasePoolの中にスタックします。
このCCAutoreleasePoolは単なる一時的なプールにすぎません。CCArrayの中にautorelease()された全てのCCObjectが格納されるのですが、その際リファレンスカウンタは繰り上がらずにそのまま格納されます。
※内部的には、CCArray::addObject()で一度retain()されて+1された後、すぐrelease()で-1されています。

そしてゲームのメインループ内の最後でCCAutoreleasePoolにスタックされてたものがクリア(removeAllObject)されます。その際にスタックされた全てのCCObjectがrelease()され、リファレンスカウンタが-1され、0になったらdeleteされるという仕組みになっています。

もちろん一度でもretain()されて+1されているCCObjectは、そのクリアの際に-1されてもm_uReferenceは1のこっており解放されません。
その子が解放されるタイミングはCCNode::removeChildCCArray::removeObjectされる時、あるいはこっちで明示的にrelease()を呼び出した瞬間となります。

ちょっとわかりづらいですが、

  • CCObjectを作成した際、m_uReferenceの初期値は1
  • autorelease()を呼び出すと、CCAutoreleasePoolにスタックされる
  • メインループの最後にCCAutoreleasePoolはクリアされる
  • m_uReferenceが1のインスタンスはその際に-1されて0となり、削除される
  • 一度でもretain()されたインスタンスは、クリア時に-1されても0にならずに残る

みたいな感じです。

メインループで毎回CCAutoreleasePoolはクリアされるので、retain()していないものはほぼ一瞬で解放されることになります。

結構驚くくらい簡単な仕組みで実装できるんですね。

cocos2d-xでメモリリークするケースとクラッシュするケース

以上のことをふまえ、このリファレンスカウンタ関連でcocos2d-xでメモリリークが発生するケースと既に解放されてクラッシュするケースとをある程度自分の為にもまとめておきます。

クラッシュするケース

これはそんなにはないかと思いますが。というか、リファレンスカウンタ関連でのクラッシュの原因はこの2つしかないかも?

■メンバ変数をretain()してない

メンバ変数にCCObjectを継承したクラスを入れる場合、生成時にちゃんとretain()しないと一瞬でCCAutoreleasePoolに持っていかれちゃいます。
ということで、基本はメンバ変数にする場合はretain()を明示的にするようにした方がいいですね(例外はもちろんあります)

■もう解放されてるのにもう一度release()した

言わずもがなですね。

メモリリークするケース

■autorelease()を呼び出してない

CCObjectやCCLayer、CCSpriteを継承したクラスを自作した際、create()メソッドなどでうっかりautorelease()を書き忘れたりすると、明示的にrelease()しない限りメモリリークします。
きっとインスタンスの作成はcreate()で統一した方がいいと思うんで、自作した場合忘れないようにしなきゃですね(`・ω・´)

■明示的にretain()を行ったのにrelease()してない

CCNode::addChild()やCCArray::addObject()といったcocos2dx内のクラスだけを使う場合、それぞれの解放時に格納した配列(子ノード)全てをrelease()してくれるのであまり気にしなくても問題ありません。
ただ、プログラマ側が明示的にretain()した場合、ちゃんとデストラクタなどでrelease()しないと解放されず、メモリリークします。

■循環参照をしてる

上記2つはすぐわかるのでほぼ問題なさげですが、一番うっかりして起こりやすそうなのがこれですね。親ノードと子ノード両方でretain()した場合、どちらもjavaでいう強参照と似たような状態になり、どちらかで明示的にrelease()を発行しないとメモリリークが起きます。

たとえば、以下のコードはメモリリークします。

//レイヤー
class MyLayer : public CCLayer{
   CCArray* m_mySpriteAry;
public:
   CREATE_FUNC(MyLayer);
   virtual bool init();
   virtual ~MyLayer();
};

//スプライト
class MySprite : public CCSprite{
   MyLayer* m_myLayer;
public:
   static MySprite* create(MyLayer* $myLayer);
   virtual bool init(MyLayer* $myLayer);
   virtual ~MySprite();
};

/**
 *  ここからソースファイル
 *  まずはMySpriteから実装(順番に意味はないですよ)
 */

MySprite* MySprite::create(MyLayer* $myLayer){
    MySprite* pRet = new MySprite();
    if(pRet && pRet->init($myLayer)){
       pRet->autorelease(); //これ忘れるとメモリリークするよ
       return pRet;
    }
    CC_SAFE_DELETE(pRet);
    pRet = NULL;
    return NULL;
}

bool MySprite::init(MyLayer* $myLayer){
  if(!CCSprite::init()) return false;  //CCSpriteの初期化
   m_myLayer = $myLayer;
   m_myLayer->retain();  //解放しないようにretain()しとかなきゃね!
   return true;
}

//デストラクタ
MySprite::~MySprite(){
  CC_SAFE_RELEASE(m_myLayer); //m_myLayerをrelease()
}
/**
 * ここからMyLayerの実装
 */
bool MyLayer::init(){
   if(!CCLayer::init()) return false; //CCLayerの初期化
   m_mySpriteAry = CCArray::create();
   m_mySpriteAry->retain(); //解放されないようにretain()しとかなきゃね!
   for(int i = 0; i < 10; i++){
      MySprite* spr = MySprite::create(this);
      m_mySpriteAry->addObject(spr);
   }
   return true;
}

//デストラクタ
MyLayer::~MyLayer(){
  CC_SAFE_RELEASE(m_mySpriteAry); //m_mySpriteAryをrelease()
}

ちなみにCREATE_FUNC、CC_SAFE_DELETE、CC_SAFE_RELEASEはcocos2dx内で定義されているマクロです。

上のコードの場合、MyLayerのデストラクタでm_mySpriteAryが解放されることになっていますが、これが呼び出されることは明示的にdeleteでもしない限りありません。
全てのMySpriteがMyLayerをretain()してリファレンスカウンタが繰り上がっている為です。
そのリファレンスカウンタが元に戻るには全てのMySpriteが解放され、デストラクタが呼び出されなくてはなりません。が、MySpriteが解放されることもやはり明示的にdeleteでもされない限りありません
MySpriteはMyLayerのm_mySpriteAryによってretain()されている為、解放は永遠にされなくなります。と、このようにデッドロック状態が発生しちゃいます。

循環参照恐ろしいですね。

循環参照によるメモリリークを防ぐ方法

上記のコードで、メモリリークを防ぐには次のような方法があります。

■子ノード側(MySprite)では親ノード(MyLayer)をretain()しない

一番簡単な方法ですね。子ノードが存在しているライフサイクルは基本親ノードのライフサイクルよりも長くはならないことが殆どでしょうから、わざわざretain()しとく必要がありません。
(MyLayerはあるCCSceneの子ノードであるとして)MySpriteがretain()してない場合、MyLayerがremoveChildされた際にリファレンスカウンタが0となり、無事デストラクタも働いてm_mySpriteAryは解放され、次々とドミノ倒し式に解放されていきます。

そのインスタンスのライフサイクルを考えて、親より長くならないとわかっているならretain()はしないようにしておけば回避できますね。

他にもMyLayerのinit()とデストラクタで行っていることをonEnter()、onExit()メソッドで行いつつonExit()でremoveAllChildren()を発行したらもしかしたら循環参照が防げるかもしれないです。
(確かめてはいないからできないかも・・・)

//上記コードの以下のメソッドをかえる
bool MyLayer::init(){
   if(!CCLayer::init()) return false; //CCLayerの初期化
   return true;
}

void MyLayer::onEnter(){
   if(m_mySpriteAry != NULL) return;
   m_mySpriteAry = CCArray::create();
   m_mySpriteAry->retain(); //解放されないようにretain()しとかなきゃね!
   for(int i = 0; i < 10; i++){
      MySprite* spr = MySprite::create(this);
      m_mySpriteAry->addObject(spr);
   }
}

void MyLayer::onExit(){
   CC_SAFE_RELEASE(m_mySpriteAry);
   m_mySpriteAry = NULL;
   removeAllChildren();
}

//デストラクタ
MyLayer::~MyLayer(){
}

まだまだやり方はたくさんあると思いますが、ぼくが考えつくのはそれくらいです(´・ω・`)

とりあえず以上の注意をしておけば(多分)循環参照には陥らないと思うけど、CCCallFuncといったクラスも内部でretain()を行うので、それ自身やそのアクションを登録したCCSequenceやCCSpawnといったインスタンスをretain()しちゃった場合、どこかでちゃんと解放しないとメモリリークが発生するので注意が必要です。

ちなみに、CCNode::onEnter()CCNode::onExit()は親ノードがaddChildとremoveChild時にそれぞれ呼び出されます。なので、CCActionをretain()した場合、onExit()でrelease()すればメモリリークを防げるかもですね。

cocos2d-xのリファレンスカウンタを理解してクラッシュやメモリリークを防ぐ” への2件のコメント

  1. ピンバック: cocos2dxで使いそうなマクロまとめ | たそがれブランチ

  2. 私も同じ問題でエラーが発生していて困っていたので、勉強になりました!
    ありがとうございます。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です