• Cocos2d-x的内存管理


    Cocos2d-x的内存管理

    要是完全没有接触过Objc, 只是了解C++, 看到cocos2d-x的内存管理设计, 会想说脏话的. 了解objc的话, 起码还能理解cocos2d-x的开发者是尝试在C++中模拟Objc的内存管理方式. 不仅仅是说加引用计数而已, 因为真要在C++中加引用计数的方法有很多种, cocos2d-x用的这种方法, 实在太不原生态了.

    简单情况

    因为cocos2d-x中牵涉到显示的情况最多, 我也就不拿CCArray这种东西做例子了, 看个CCSprite的例子吧, 用cocos2d-x的XCode template生成的HelloWorld工程中, 删除原来的显示代码, 创建一个Sprite并显示的代码如下:

    // part code of applicationDidFinishLaunching in AppDelegate.cpp
    // create a scene. it's an autorelease object
    CCScene *scene = HelloWorld::scene();
    
    CCSprite *helloworld = new CCSprite;
    if (helloworld->initWithFile("HelloWorld.png")) {
      CCSize size = CCDirector::sharedDirector()->getWinSize();
      // position the sprite on the center of the screen
      helloworld->setPosition( ccp(size.width/2, size.height/2) );
    
      // add the sprite as a child to this layer
      scene->addChild(helloworld, 0);
      helloworld->release();
    }
    

    这里暂时不管HelloWorld::scene, 先关注CCSprite的创建和使用, 这里使用new创建了CCSprite, 然后使用scene的addChild函数, 添加的到了scene中, 并显示. 一段这样简单的代码, 但是背后的东西却很多, 比如, 为啥我在scene的addChild后, 调用了sprite的release函数呢?
    还是可以从引用计数的所有权上说起(这样比较好理解, 虽然你也可以死记哪些时候具体引用计数的次数是几). 当我们用new创建了一个Sprite时, 此时Sprite的引用计数为1, 并且所有权属于helloworld这个指针, 我们在把helloworld用scene的addChild函数添加到scene中后, helloworld的引用计数此时为2, 由helloworld指针和scene共享所有权, 此时, helloworld指针的作用其实已经完了, 我们接下来也不准备使用这个指针, 所有权留着就再也释放不了了, 所以我们用release方法特别释放掉helloworld指针此时的所有权, 这么调用以后, 最后helloworld这个Sprite所有权完全的属于scene.
    但是我们这么做有什么好处呢? 好处就是当scene不想要显示helloworld时, 直接removeChild helloworld就可以了, 此时没有对象再拥有helloworld这个sprite, 引用技术为零, 这个sprite会如期的释放掉, 不会导致内存泄漏.
    比如说下列代码:

    // create a scene. it's an autorelease object
    CCScene *scene = HelloWorld::scene();
    
    //  CCSprite* sprite = CCSprite::create("HelloWorld.png");
    CCSprite *helloworld = new CCSprite;
    if (helloworld->initWithFile("HelloWorld.png")) {
      CCSize size = CCDirector::sharedDirector()->getWinSize();
      // position the sprite on the center of the screen
      helloworld->setPosition( ccp(size.width/2, size.height/2) );
    
      // add the sprite as a child to this layer
      scene->addChild(helloworld, 0);
      helloworld->release();
    
      scene->removeChild(helloworld);
    }
    

    上面的代码helloworld sprite能正常的析构和释放内存, 假如少了那句release的代码就不行.

    容器对引用计数的影响

    这个部分是引用计数方法都会碰到的问题, 也就是引用计数到底在什么时候增加, 什么时候减少.
    在cocos2d-x中, 我倒是较少会像在objc中手动的retain对象了, 主要的对象主要由CCNode和CCArray等容器管理. 在cocos2d-x中, 以CC开头的, 模拟Objc接口的容器, 都是对引用计数有影响的, 而原生的C++容器, 对cocos2d-x的对象的引用计数都没有影响, 这导致了人们使用方式上的割裂. 大部分用惯了C++的人, 估计都还是偏向使用C++的原生容器, 毕竟C++的原生容器及其配套算法算是C++目前为数不多的亮点了, 比objc原生的容器都要好用, 更别说Cocos2d-x在C++中模拟的那些objc容器了. 但是, 一旦走上这条路就需要非常小心, 要非常明确此时每个对象的所有权是谁.
    看下面的代码:

    vector<CCSprite*> sprites;
    for (int i = 0; i < 3; ++i) {
      CCSprite *helloworld = new CCSprite;
      if (helloworld->initWithFile("HelloWorld.png")) {
        CCSize size = CCDirector::sharedDirector()->getWinSize();
        // position the sprite on the center of the screen
        helloworld->setPosition( ccp(size.width/2, size.height/2) );
    
        // add the sprite as a child to this layer
        scene->addChild(helloworld, 0);
        sprites.push_back(helloworld);
        helloworld->release();
    
        scene->removeChild(helloworld);
      }
    }
    

    因为C++的容器是对Cocos2d-x的引用计数没有影响的, 所以在上述代码运行后, 虽然vector中保存者sprite的指针, 但是其实都已经是野指针了, 所有的sprite实际已经析构调了. 这种情况相当危险. 把上述代码中的vector改成cocos2d-x中的CCArray就可以解决上面的问题, 因为CCArray是对引用计数有影响的.
    见下面的代码:

    CCArray *sprites = CCArray::create();
    for (int i = 0; i < 3; ++i) {
      CCSprite *helloworld = new CCSprite;
      if (helloworld->initWithFile("HelloWorld.png")) {
        CCSize size = CCDirector::sharedDirector()->getWinSize();
        // position the sprite on the center of the screen
        helloworld->setPosition( ccp(size.width/2, size.height/2) );
    
        // add the sprite as a child to this layer
        scene->addChild(helloworld, 0);
        sprites->addObject(helloworld);
        helloworld->release();
    
        scene->removeChild(helloworld);
      }
    }
    

    改动非常小, 仅仅是容器类型从C++原生容器换成了Cocos2d-x从Objc模拟过来的array, 但是这段代码执行后, sprites中的sprite都可以正常的使用, 并且没有问题. 可参考cocos2d-x的源代码ccArray.cpp:

    /** Appends an object. Behavior undefined if array doesn't have enough capacity. */
    void ccArrayAppendObject(ccArray *arr, CCObject* object)
    {
        CCAssert(object != NULL, "Invalid parameter!");
        object->retain();
      arr->arr[arr->num] = object;
      arr->num++;
    }
    

    但是, 假如我就是想用C++原生容器, 不想用CCArray怎么办呢? 需要承担的风险就来了, 有的时候还行, 比如上例, 我只需要去掉helloworld->release那一行, 并且明白此时所有权已经是属于vector了, 在vector处理完毕后, 再release即可.
    而有的时候这就没有那么简单了. 特别是Cocos2d-x因为依赖引用计数, 不仅仅是addChild等容器添加会增加引用计数, 回调的设计(模拟objc中的delegate)也会对引用计数有影响的. 曾经有人在初学Cocos2d-x的时候, 问我cocos2d-x有没有什么设计问题, 有没有啥坑, 我觉得这就是最大的一个.
    举个简单的例子, 我真心不喜欢引用计数, 所以全用C++的容器, 写了下面这样的代码: (未编译测试, 纯示例使用)

    class Enemy 
    {
      public:
        Enemy() {}
        ~Enemy() {}
    
    };
    
    
    class EnemyManager 
    {
      public:
        EnemyManager() {}
        ~EnemyManager() {}
    
        void RemoveEnemies() {
          for (auto it : enemies_) {
            delete *it;
          }
        }
    
    private:
      vector<Enemy*> enemies_;
    };
    

    刚开始的时候, 这只是一段和Cocos2d-x完全没有关系的代码, 并且运行良好, 有一天, 我感觉的Enmey其实是个Sprite就方便操作了. 将Enemy改为继承自Sprite, 那么这段代码就没有那么安全了, 因为EnemyManager在完全不知道enemy的引用计数的情况下, 使用delete删除了enmey, 假如此时还有其他地方对该enemy有引用, 就会crash. 虽然表面上看来是想添加一些CCSprite的显示功能, 但是实际上, 一入此门(从CCObject继承过来), 引用计数就已经无处不在, 此时需要把直接的delete改为调用release函数.

    内存池

    cocos2d-x起始也模拟了objc中的内存池, 但是因为不可能改变语言本身的特性, 那种简单的语法糖语法就没有, 需要的时候, 老实的操作CCPoolManager和CCAutoreleasePool吧. 在通常情况下, cocos2d-x增加的机制使得我们不太需要像在objc中那样使用内存池. 我来解释一下:
    在cocos2d-x中, 几乎所有有意义的类都有create函数, 比如Sprite的create函数:

    CCSprite* CCSprite::create()
    {
        CCSprite *pSprite = new CCSprite();
        if (pSprite && pSprite->init())
        {
            pSprite->autorelease();
            return pSprite;
        }
        CC_SAFE_DELETE(pSprite);
        return NULL;
    }
    

    基本只干两个事情, 一个是new和init, 一个就是调用autorelease函数讲sprite本身加入内存池了. 此时讲sprite加入内存池后, sprite的所有权已经属于内存池了, 我们返回的指针其实是没有所有权的. 在create出一个类似对象后, 我们接下来的操作往往是吧这个对象再添加到parent node中(比如上层的scene或layer), 此时由内存池和这个parent node共同拥有这个sprite, 当sprite不需要再显示的时候, 直接通过removeChild将sprite从父节点中移除后, 就回到仅属于内存池的情况了.
    在objc中, 要是都是上面的情况, 我们又不手动的清理内存池, 这其实就已经有内存泄漏了, 但是cocos2d-x实际是每帧都帮我们清理内存池的. 也就是说, 每一帧仅仅属于内存池的对象都会被释放. 见下面的代码:

    void CCDisplayLinkDirector::mainLoop(void)
    {
        if (m_bPurgeDirecotorInNextLoop)
        {
            m_bPurgeDirecotorInNextLoop = false;
            purgeDirector();
        }
        else if (! m_bInvalid)
         {
             drawScene();
    
             // release the objects
             CCPoolManager::sharedPoolManager()->pop();        
         }
    }
    

    上面的代码是CCDirector的游戏主循环代码, 主循环干了件非常重要的事情, 那就是pop最上层的autorelease pool, 此时是在release全部仅仅由此内存池所有的对象. 就是依靠这样的原理, 我们可以放心的将对象放在autorelease pool中, 知道在需要的时候, 这个对象就能正确的释放, 同时只要有上层的父节点通过addChild对游戏对象有了所有权以后, 又能正确的保证该对象不会被删除.

    小结

    本文原来是来自于给公司做的内部培训材料, 因为一开始写的很初略和简单, 一直就没想发布, 最近我在整理老的资料, 所以今天整理了一下, 添加了一些例子, 发布出来了, 可以明显的看到后面的内容虽然更加重要, 但是写的比前面要仓促, 有错误的话, 请各位不吝赐教.

  • 相关阅读:
    IDEA中用好Lombok,撸码效率至少提升5倍
    在 IDEA 中使用 Debug,真是太厉害了!
    彻底理解cookie,session,token
    优秀的程序员一定要多写博客!
    IntelliJ IDEA 从入门到上瘾教程,2019图文版!
    注解配置
    过滤器(登录认证)
    过滤器
    Session监听器
    request监听器
  • 原文地址:https://www.cnblogs.com/alsky/p/3197028.html
Copyright © 2020-2023  润新知