Cocos2d-x学习笔记—内存管理机制

Cocos2d-x 3.x内存管理机制

1:C++内存管理

1-1:内存分配区域

创建对象需要两个步骤:第一步,为对象分配内存;第二步,调用构造函数初始化内存。在第一步中,可以选择几个不同的分配区域。这几个区域如下:

(1) 栈区域分配。栈内存分配运算内置于处理器的指令集中,效率很髙,但是分配的内 存容量有限。由处理器自动分配和释放,用来存放函数的参数值和局部变量的值等。在执 行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。

(2) 堆区域分配。从堆上分配,也称动态内存分配。由开发人员分配释放,如果不释 放,程序结束时由操作系统回收。程序在运行时用malloc或new申请任意多少的内存,开发人员自己负责在何时用free或delete释放内存。动态内存的生存期由开发人员决定,使用非常灵活,但问题也最多。

(3) 在静态存储区域分配。这个内存空间在程序的整个运行期间都存在,内存在程序编译的时候就已经分配好。它可以分配全局变量和静态变量。

1-2:动态内存分配

动态内存分配最为灵活但是问题也很多,这里重点介绍动态内存分配。动态内存使用 malloc或new分配内存,使用free或delete释放内存。其中,malloc和free是成对的,new 和delete是成对的。

(1)malloc和free的使用

malloc和free是C/C++语言的标准库函数,主要是在C中使用。使用malloc创建对 象,不会自动调用构造函数初始化内存。使用free释放对象,不会自动调用析构函数清除内存。

(2)new和delete的使用

与malloc和free不同,new和delete不是函数库,而是C++的运算符。new运算符能 够完成创建对象所有步骤(即第一步,为对象分配内存;第二步,调用构造函数初始化内 存),它也会调用构造函数。实例代码如下:

MyObject * obj = new MyObject();

构造函数可以重载,根据用户传递的参数列表,决定调用哪个构造函数进行初始化对象。

new运算符的反操作运算符是delete,delete先调用析构函数,再释放内存。实例代码如下:

delete obj;

其中,obj是对象指针,obj只能释放new创建的对象,不能释放由malloc创建的。而且采 用delete释放后的对象指针,需要obj = NULL以防止“野指针”。

提示:一种情况是,指针变量没有被初始化,它的指向是随机的,它会乱指一气,并不是NULL。如果使用if语句判断,则认为是有效指针。另一种情况是,指针变量被free或者 delete之后,它们只是把指针所指的内存释放掉,但并没有把指针本身清除,此时指针指向的就是“垃圾”内存。如果使用if语句判断,也会认为是有效指针。“野指针”是很危险的,良好的编程习惯是,这两种情况下都需要将指针设置为NULL。这是避免“野指针”的唯一方法。

2:Cocos2d-x内存管理

在3.x版本,Cocos2d-x采用全新的根类Ref ,实现Cocos2d-x类对象的引用计数记录。引擎中的所有类都派生自Ref。Cocos2d-x内存管理是建立在C++语言new/delete之上,通过引入Object-C语言的引用计数来实现的。

2-1:内存引用计数

Ref类设计来源于Cocos2d-iphone的CCObject 类,在Cocos2d-x 2.x中也叫CCObject类。因此Ref类的内存管理是参考Objectives手动管理引用计数(reference count,RC)而设计的。

每个Ref对象都有一个内部计数器,这个计数器跟踪对象的引用次数,被称为“引用计数”(RC)。当对象被创建时,引用计数为1。为了保证对象的存在,可以调用retain函数保 持对象,retain会使其引用计数加1,如果不需要这个对象可以调用release函数,release使其引用计数减1。当对象的引用计数为0时,引擎就知道不再需要这个对象了,就会通过delete释放对象内存。

核心类Ref:实现了引用计数。

 /**
 * CCRef.h
 **/
class CC_DLL Ref
{
    public:
        void retain();                              // 保留。引用计数+1
        void release();                             // 释放。引用计数-1
        Ref* autorelease();                         // 实现自动释放。
        unsigned int getReferenceCount() const;     // 被引用次数
    protected:
        Ref();                                      // 初始化
    public:
        virtual ~Ref();                            // 析构
    protected:
        unsigned int _referenceCount;               // 引用次数
        friend class AutoreleasePool;               // 自动释放池
};

/**
 * CCRef.cpp
 **/

// 节点被创建时,引用次数为 1
Ref::Ref() : _referenceCount(1)
{
}

void Ref::retain()
{
    CCASSERT(_referenceCount > 0, "reference count should greater than 0");
    ++_referenceCount;
}

void Ref::release()
{
    CCASSERT(_referenceCount > 0, "reference count should greater than 0");
    --_referenceCount;
    if (_referenceCount == 0)
    {
        delete this;
    }
}

Cocos2d-x提供引用计数管理内存的方法如下:

  1. 调用 retain()方法:令其引用计数增1,表示获取该对象的引用权。
  2. 调用 release()方法:在引用结束的时候,令其引用计数值减1,表示释放该对象的引用权。
  3. 调用 autorelease()方法 :将对象放入自动释放池。当释放池自身被释放的时候,它就会对池中的所有对象执行一次release()方法,实现灵活的垃圾回收。Cocos2d-x提供AutoreleasePool,管理自动释放对象。当释放池自身被释放的时候,它就会对池中的所有对象执行一次release()方法。

Ref原理分析:

  1. 当一个Ref初始化(被new出来时),_referenceCount = 1;
  2. 当调用该Ref的retain()方法时,_referenceCount++;
  3. 当调用该Ref的release()方法时,_referenceCount–;
  4. 若_referenceCount减后为0,则delete该Ref。

2-2:autorelease使用

当一个继承自Ref的obj对象创建后,其引用计数_referenceCount为1;执行一次autorelease()后,obj对象被加入到当前的自动释放池AutoreleasePool,它能够管理即将释放的对象池。obj对象的引用计数值并没有减1。但是在一帧结束时刻或者称一个消息循环结束时刻,当前的自动释放池会被回收掉,并对自动释放池中的所有对象执行一次release()操作,当对象的引用计数为0时,对象会被释放掉。

所谓所谓一帧或者一个消息循环,即是一个gameloop(在导演类中)。每次为了处理新的事件,Cocos2d-x引擎都会创建一个新的自动释放池,事件处理完成后,就会销毁这个池,池中对象的引用计数会减1,如果这个引用计数会减0,也就是没有被其它类或Ref对象retain,则释放对象,否则这个对象不会释放,在这次销毁池过程中“幸存”下来,它被转移到下一个池中继续生存。总结起来就是:

  1. 系统在每一帧结束时都会销毁当前自动释放池,并创建一个新的自动释放池;
  2. 自动释放池在销毁时会对池中的所有对象执行一次release()操作;
  3. 新创建的对象如果一帧内不使用,就会被自动释放;

2-2-1 autorelease源码如下:

Ref* Ref::autorelease()
{
    // 将节点加入自动释放池
    PoolManager::getInstance()->getCurrentPool()->addObject(this);
    return this;
}

2-2-2 与autorelease相关的类如下:

(1)AutoreleasePool类: 管理一个 vector 数组来存放加入自动释放池的对象。提供对释放池的清空操作。

// 存放释放池对象的数组
std::vector<Ref*> _managedObjectArray;

// 往释放池添加对象
void AutoreleasePool::addObject(Ref* object)
{
    _managedObjectArray.push_back(object);
}

// 清空释放池,将其中的所有对象都 delete
void AutoreleasePool::clear()
{
    // 释放所有对象
    for (const auto &obj : _managedObjectArray)
    {
    obj->release();
    }
    // 清空vector数组
    _managedObjectArray.clear();
}

// 查看某个对象是否在释放池中
bool AutoreleasePool::contains(Ref* object) const
{
    for (const auto& obj : _managedObjectArray)
    {
        if (obj == object)
        return true;
    }
    return false;
}

(2)PoolManager 类: 管理一个 vector 数组来存放自动释放池。默认情况下引擎只创建一个自动释放池,因此这个类是提供给开发者使用的,例如出于性能考虑添加自己的自动释放池。

// 释放池管理器单例对象
static PoolManager* s_singleInstance;

// 释放池数组
std::vector<AutoreleasePool*> _releasePoolStack;

// 获取 释放池管理器的单例
PoolManager* PoolManager::getInstance()
{
    if (s_singleInstance == nullptr)
    {
        // 新建一个管理器对象
        s_singleInstance = new PoolManager();
        // 添加一个自动释放池
        new AutoreleasePool("cocos2d autorelease pool");// 内部使用了释放池管理器的push,这里的调用很微妙,读者可以动手看一看
    }
    return s_singleInstance;
}

// 获取当前的释放池
AutoreleasePool* PoolManager::getCurrentPool() const
{
    return _releasePoolStack.back();
}

// 查看对象是否在某个释放池内
bool PoolManager::isObjectInPools(Ref* obj) const
{
    for (const auto& pool : _releasePoolStack)
    {
        if (pool->contains(obj))
        return true;
    }
    return false;
}

// 添加释放池对象
void PoolManager::push(AutoreleasePool *pool)
{
    _releasePoolStack.push_back(pool);
}

// 释放池对象出栈
void PoolManager::pop()
{
    CC_ASSERT(!_releasePoolStack.empty());
    _releasePoolStack.pop_back();
}

我们可以自己创建AutoreleasePool,管理对象的autorelease。

我们已经知道,调用了autorelease()方法的对象(下面简称”autorelease对象”),将会在自动释放池释放的时候被释放一次。虽然,Cocos2d-x已经保证每一帧结束后释放一次释放池,并在下一帧开始前创建一个新的释放池,但是我们也应该考虑到释放池本身维护着一个将要执行释放操作的对象列表,如果在一帧之内生成了大量的autorelease对象,将会导致释放池性能下降。因此,在生成autorelease对象密集的区域(通常是循环中)的前后,我们最好可以手动创建并释放一个回收池。

(3)DisplayLinkDirector 类: 这是一个导演类,提供游戏的主循环,实现每一帧的资源释放。这个类的名字看起来有点怪,但是不用管它。因为这个类继承了 Director 类,也是唯一一个继承了 Director 的类,也就是说完全可以合并为一个类,引擎开发者在源码中有部分说明。

void DisplayLinkDirector::mainLoop()
{
    //第一次当导演
    if (_purgeDirectorInNextLoop)
    {
        _purgeDirectorInNextLoop = false;
        purgeDirector();//进行清理工作
    }
    else if (! _invalid)
    {
        // 绘制场景,游戏主要工作都在这里完成
        drawScene();
        // 清空资源池
        PoolManager::getInstance()->getCurrentPool()->clear();
    }
}

2-2-3 总结:

  1. autorelease()的实质是将对象加入自动释放池,对象的引用计数不会立刻减1,在自动释放池被回收时对象执行release()。
  2. autorelease()只有在自动释放池被释放时才会进行一次释放操作,如果对象释放的次数超过了应有的次数,则这个错误在调用autorelease()时并不会被发现,只有当自动释放池被释放时(通常也就是游戏的每一帧结束时),游戏才会崩溃。在这种情况下,定位错误就变得十分困难了。例如,在游戏中,一个对象含有1个引用计数,但是却被调用了两次autorelease()。在第二次调用autorelease()时,游戏会继续执行这一帧,结束游戏时才会崩溃,很难及时找到出错的地点。因此,我们建议在开发过程中应该避免滥用autorelease(),只在工厂方法等不得不用的情况下使用,尽量以release()来释放对象引用。
  3. autorelease()并不是毫无代价的,其背后的释放池机制同样需要占用内存和CPU资源。过多的使用autorelease()会增加自动释放池的管理和释放池维护对象存取释放的支出。在内存和CPU资源本就不足的程序中使得系统资源更加紧张。此时就需要我们合理创建自动释放池管理对象autorelease。
  4. 不用的对象推荐使用release()来释放对象引用,立即回收。

3:Ref特殊内存管理

(1)Node的addChild/removeChild方法

在Cocos2d-x中,所有继承自Node类,在调用addChild方法添加子节点时,子节点会自动调用了retain。 对应的通过removeChild,移除子节点时,子节点会自动调用了release。

调用addChild方法添加子节点,节点对象执行retain。子节点被加入到节点容器中,父节点销毁时,会销毁节点容器释放子节点,即对子节点执行release。如果想提前移除子节点我们可以调用removeChild。

在Cocos2d-x内存管理中,大部分情况下我们通过调用addChild/removeChild的方式自动完成了retain,release调用。不需再调用retain,release。

(2)树形结构和链式反应

我们当前运行这一个场景,场景初始化,添加了很多层,层里面有其它的层或者精灵,而这些都是CCNode节点,以场景为根,形成一个树形结构,场景初始化之后(一帧之后),这些节点将完全 依附(内部通过retain)在这个树形结构之上,全权交由树来管理,当我们砍去一个树枝,或者将树连根拔起,那么在它之上的“子节点”也会跟着去除(内部通过release),这便是链式反应。

(3)工厂方法

在Cocos2d-x中,提供了大量的工厂方法create静态函数创建对象。仔细看你会发现,这些对象都是自动释放的。

#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;
    }
}

下面以一段代码说明Cocos2d-x对象自动释放过程。

void HelloWorld::test()
{
    auto lable = Lable::create();
    this->addChild(lable);
}
  1. 首先通过create方法创建了一个lable(创建过程中执行lable->autorelease()),对象被加入到当前的自动释放池AutoreleasePool中,此时label的引用计数为1;
  2. 执行this->addChild(lable)将label作为子节点加入Layer父节点中,此时label的引用计数为2;
  3. 当一帧结束后,当前的自动释放池AutoreleasePool被释放,对label执行了一次release操作,label的引用计数变为1,当前的自动释放池被销毁,lable->autorelease()的作用结束;
  4. 当游戏结束或者是切换场景时,label的父节点Layer会被销毁,发生链式反应,Layer对其子节点label执行release操作,label的引用计数变为0,执行delete label操作,内存得以回收。

4:内存管理使用建议

(1)类内部使用new分配的内存,在析构函数中执行delete操作;

(2)cocos2d-x的类元素通过create静态工厂创建,其内存会通过autorelease自动管理,无需其他操作;

(3)每个retain函数一定要对应一个release函数或一个autorelease函数;

(4)单例模式下由于只有一个实例,可以写一个成员函数用于delete对象指针,在游戏结束时执行该成员函数就可以了;

时间: 2024-10-20 05:42:26

Cocos2d-x学习笔记—内存管理机制的相关文章

JVM学习笔记-内存管理

第一章 内存分配 1. 内存区域. 方法区和堆(线程共享),程序计数器 , VM栈 和 本地方法栈(线程隔离). 1) java虚拟机栈:线程私有.描述的是java方法执行的内存模型:栈帧,用户存储 局部变量表,操作数栈,动态链接,方法出口等信息. 局部变量表在编译时即可完全确定!如果线程请求的栈深度大于 规定的深度,StackOverflowError. 2) 本地方法栈,类似. 3)堆:垃圾收集器管理的主要区域.线程共享. 4)方法区: 各个线程共享.存储:加载的类信息,常量,静态变量,即时

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

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

2015 IOS 学习笔记 内存管理,页面跳转 ——蓝懿教育

——————————内存管理———————————————— 手动内存管理(MRC)中常用的三个方法 retain:导致内存计数+1 release:导致内存计数-1 copy:复制出来一个新的对象 和之前对象的数据可能一致 但是 不是同一个对象 此对象内存计数是1 autorelease:自动释放 当变量出了自动释放池之后会自动释放 自动释放池在项目中有很多看不见的 属性描述关键字: retain/strong: //如果是retain 会做两件事 //-(void)setNames:(NSM

23-黑马程序员------OC 语言学习笔记---内存管理

黑马程序员------<a href="http://www.itheima.com" target="blank">Java培训.Android培训.iOS培训..Net培训</a>.期待与您交流! ------- 对于面向对象的变成语言,程序需要不断地创建对象.初始,创建的所有程序通常都有指针指向它,程序可能需要访问这些对象的实例变量或调用这些对象的方法,随着程序的不断执行,程序再次创建了一些新的对象,而那些老的对象已经不会再被调用,也不

C++学习笔记-内存管理与指针

一.使用new和delete时,应遵循以下规则: 1.不要使用delete来释放不是new分配的内存. 2.不要使用delete释放同一个内存块两次. 3.如果使用new[]为数组分配内存,则应使用delete[]来释放. 4.如果使用new[]为一个实体分配内存,则应使用delete(没有方括号)来释放. 5.对空值指针应用delete是安全的. 二.句点操作符和箭头操作符的规则: 1.如果结构标识符是结构名,则使用句点操作符: 2.如果标识符是指向结构的指针,则应使用箭头操作符(->)

Objective-C学习笔记 内存管理

引用计数 每个对象都有一个与之相关联多整数,被称作它的引用计数器或保留计算器,当某段代码需要访问一个对象时,该代码就将该对象的保留计数器值加1,表示我要访问该对象,当这段代码访问结束的时候,将对象的保留计数器数值减1,表示不再访问该对象了.当保留计数器的值为0时,表示不再有代码访问该对象了,此时它将被销毁同时占用的内存被回收. 对象的保留计数器值初始值为1.当一个对象即将要被销毁的时候,OC会向对象发送一条dealloc消息,这条消息可以在自己的对象中重写. 常见的几种调用方法: -(id) r

cocos2d 内存管理机制

简单做下笔记,等有更深的理解时再补充. Cocos2d内存管理的基本原理是对象内存引用计数.当声明定义一个对象时,会在堆上为这个对象分配内存,并且有一个变量m_uReference专门用于记录该对象被引用了多少次. 内存引用计数的原理就是,当该对象被引用时m_uReference++,当该对象被取消引用时m_uReference--,若果m_uReference==0时,该对象就会被释放. 有了基本概念后,就不得不提及两个重要的函数--retain()和release().这两个函数是干什么用的

Linux内存管理学习笔记——内存寻址

最近开始想稍微深入一点地学习Linux内核,主要参考内容是<深入理解Linux内核>和<深入理解Linux内核架构>以及源码,经验有限,只能分析出有限的内容,看完这遍以后再更深入学习吧. 1,内存地址 逻辑地址:包含在机器语言中用来指定一个操作数或一条指令的地址. 线性地址:一个32位无符号数,用于直接映射物理地址 物理地址:片上引脚寻址级别的地址 2,逻辑地址->线性地址 2.1 段选择符与段寄存器 逻辑地址:段选择符(16位)+段内偏移(32位) index:在GDT或L

《python源码剖析》笔记 pythonm内存管理机制

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.内存管理架构 Python的内存管理机制都有两套实现:debug模式和release模式 Python内存管理机制的层次结构: 图16-1 第0层是操作系统提供的内存管理接口,如malloc.free 第1层是Python基于第0层操作系统的内存管理接口包装而成的,主要是为了处理与平台相关的内存分配行为. 实现是一组以PyMem_为前缀的函数族 两套接口:函数和宏. 宏,可以避免函数调