菜鸟学习Cocos2d-x 3.x——内存管理

菜鸟学习Cocos2d-x 3.x——内存管理

2014-12-10 分类:Cocos2d-x / 游戏开发 阅读(394) 评论(6)

亘古不变的东西

到现在,内存已经非常便宜,但是也不是可以无限大的让你去使用,特别是在移动端,那么点内存,那么多 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。这些地方稍微不注意,就可能会让你掉入“坑”中。 努力吧,伙计们。

2014年12月10日 于深圳。

时间: 2024-10-10 08:04:05

菜鸟学习Cocos2d-x 3.x——内存管理的相关文章

Cocos2d之&ldquo;引用计数&rdquo;内存管理机制实现解析

一.引言 本文主要分析cocos2d游戏开发引擎的引用计数内存管理技术的实现原理.建议读者在阅读本文之前阅读笔者之前一篇介绍如何使用cocos2d内存管理技术的文章--<Cocos2d之Ref类与内存管理使用详解>. 二.相关概念 引用计数 引用计数是计算机编程语言的一种内存管理技术,是指将资源(对象.内存或者磁盘空间等)的被引用计数保存起来,当引用计数变为零时就将资源释放的过程.使用引用计数技术可以实现自动内存管理的目的. 当实例化一个类时,对象的引用计数为1,在其他对象需要持有这个对象时,

【Spark-core学习之八】 SparkShuffle &amp; Spark内存管理

[Spark-core学习之八] SparkShuffle & Spark内存管理环境 虚拟机:VMware 10 Linux版本:CentOS-6.5-x86_64 客户端:Xshell4 FTP:Xftp4 jdk1.8 scala-2.10.4(依赖jdk1.8) spark-1.6 一.SparkShuffle1. SparkShuffle概念reduceByKey会将上一个RDD中的每一个key对应的所有value聚合成一个value,然后生成一个新的RDD,元素类型是<key,v

Java之美[从菜鸟到高手演变]之JVM内存管理及垃圾回收

很多Java面试的时候,都会问到有关Java垃圾回收的问题,提到垃圾回收肯定要涉及到JVM内存管理机制,Java语言的执行效率一直被C.C++程序员所嘲笑,其实,事实就是这样,Java在执行效率方面确实很低,一方面,Java语言采用面向对象思想,这也决定了其必然是开发效率高,执行效率低.另一方面,Java语言对程序员做了一个美好的承诺:程序员无需去管理内存,因为JVM有垃圾回收(GC),会去自动进行垃圾回收. 其实不然: 1.垃圾回收并不会按照程序员的要求,随时进行GC. 2.垃圾回收并不会及时

IOS 阶段学习第九天笔记(内存管理)

IOS学习(C语言)知识点整理 一.内存管理 1)malloc , 用于申请内存; 结构void *malloc(size_t),需要引用头文件<stdlib.h>:在堆里面申请内存,size_t,表示申请空间的大小,单位是字节:如果申请成功,返回这段内存的首地址,申请失败,返回NULL;需要手动初始化 注意点: 1.可能会申请失败,所以需要判断返回是否是NULL. 2.申请的内存需要强制转换为指定的数据类型,例如:(int*)malloc(10*sizeof(int)) 3.分配的内存是未初

分布式memcached学习(三)&mdash;&mdash;memcached内存管理机制

  几个重要概念 Slab memcached通过slab机制进行内存的分配和回收,slab是一个内存块,它是memcached一次申请内存的最小单位,.在启动memcached的时候一般会使用参数-m指定其可用内存,但是并不是在启动的那一刻所有的内存就全部分配出去了,只有在需要的时候才会去申请,而且每次申请一定是一个slab.Slab的大小固定为1MB(1MB=1024KB=1024×1024B=1048576B,1048576字节),一个slab由若干个大小相等的chunk组成. Slab的

《objective-c基础教程》学习笔记(十)—— 内存管理之对象生命周期

本篇博文,将给大家介绍下再Objective-C中如何使用内存管理.一个程序运行的时候,如果不及时的释放没有用的空间内存.那么,程序会越来越臃肿,内存占用量会不断升高.我们在使用的时候,就会感觉很卡,最终使得程序运行奔溃.因此,将无效的内存及时清理释放,是非常有必要的. 一个对象在最初创建使用,到最后的回收释放,经历的是怎样一个过程呢?包括:诞生(通过alloc或new方法实现).生存(接收消息并执行操作).交友(通过复合以及向方法传递参数).最终死去(被释放掉). 一.引用计数 在对象创建的时

(Object-C)学习笔记(三) --OC的内存管理、封装、继承和多态

OC的内存管理 iOS7以前使用的是MRC手动内存管理,现在都使用ARC自动内存管理,一般不会出现内存泄漏问题. 封装 封装就是有选择的保护自己的代码.将给别人使用的接口留出来让人看见,其他的都隐藏起来.增加了代码的可读性.可维护性.可拓展性. 将给别人看的代码放在 interface当中(.h or .m),不让看的放在implementation当中.这就是封装对象. 继承 子类可以继承父类非私有的属性和方法. 继承的优点:1.代码复用 2.制定规则 3.为了多态 继承的缺点:1.提高了代码

【OC学习-11】ARC和内存管理里面的alloc、assign、new、retain、copy、mutableCopy、release说明

一般我们在开发程序时,只管alloc,不需要管release,因为有ARC帮我们管理.但是在学习时仍需要了解:内存是有限的,在堆区分配了内存后,如果不需要,则要回收,不然内存不够引起崩溃. 所以原则是:有分配,就有回收.但是这个分配有可能分配好几次,那么回收怎么知道回收几次呢?这就是引用计数的作用.创建一个对象时,它自带了这个引用计数. (1)alloc.new.copy.mutableCopy和retain时,引用计数+1,即retainCount+1; (2)release时,引用计数ret

JVM学习笔记(三)------内存管理和垃圾回收【转】

转自:http://blog.csdn.net/cutesource/article/details/5906705 版权声明:本文为博主原创文章,未经博主允许不得转载. JVM内存组成结构 JVM栈由堆.栈.本地方法栈.方法区等部分组成,结构图如下所示: 1)堆 所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制.堆被划分为新生代和旧生代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由From Space和To Space组成,结构图

C++内存管理学习笔记(3)

/****************************************************************/ /*            学习是合作和分享式的! /* Author:Atlas                    Email:[email protected] /*  转载请注明本文出处: *   http://blog.csdn.net/wdzxl198/article/details/9078965 /************************