目标读者:了解 Cocos2d-x 中的节点以及节点树,了解引用计数,了解游戏主循环等概念。


本文首先介绍 Cocos2d-x 3.2 中内存管理的作用,以及各个作用的应用。借由通俗易懂的解释来了解内存管理的过程。其次通过源码解析介绍其内部的实现原理。加深理解,从而在有需要的时候绕开引擎建立自己的内存管理机制。

一、Cocos2d-x 3.2 内存管理的两个方面

  1. 通过加入 autorelease 来自动释放那些创建后未使用的对象。
  2. 通过节点管理来保证对象在弃用后及时地删除。

1、及时释放弃用的对象

使用条件:该对象是Node的子类对象

使用方法:addChild、removeChild

内存管理过程:

addChild //添加对象后,对象可以被使用
removeChild //删除对象后,对象被立刻删除(通过 delete)

2、及时释放未使用的对象

简述新创建的对象如果一帧内不使用,就会被自动释放。(所谓一帧,即是一个gameloop。)

使用条件:对象通过CREAT_FUNC()宏创建或者对象使用autorelease加入了自动释放池。

使用方法:自动实现

内存管理过程:

  • 对象不使用的情况
对象创建 引用+1
对象自动释放 引用-1
  • 对象使用的情况
对象创建 引用+1
对象使用 引用+1 // 通过 addChild 使用对象
对象自动释放 引用-1

引用的初始值为0,如果一帧结束后对象的引用值还是0,那就就会被 delete。



二、内存管理的实现原理

涉及内存管理的文件很多,仅展示直接相关的部分代码。

1、第一部分

Ref 类: 进行引用计数、提供加入自动释放池的接口。

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

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

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

1.1 Ref

// 引用计数变量
unsigned int _referenceCount; // 对象被构造后,引用计数值为 1
Ref::Ref()
: _referenceCount(1) //当Ref对象被创建时,引用计数的值为 1
{
#if CC_ENABLE_SCRIPT_BINDING
static unsigned int uObjectCount = 0;
_luaID = 0;
_ID = ++uObjectCount;
_scriptObject = nullptr;
#endif #if CC_USE_MEM_LEAK_DETECTION
trackRef(this);
#endif
} // 引用+1
void Ref::retain()
{
CCASSERT(_referenceCount > 0, "reference count should greater than 0");
++_referenceCount;
} // 引用-1 。如果引用为0则释放对象
void Ref::release()
{
CCASSERT(_referenceCount > 0, "reference count should greater than 0");
--_referenceCount; if (_referenceCount == 0)
{ #if CC_USE_MEM_LEAK_DETECTION
untrackRef(this);
#endif
delete this; // 注意这里 把对象 delete 了
}
} // 提供加入自动释放池的接口。对象调用此函数即可加入自动释放池的管理。
Ref* Ref::autorelease()
{
PoolManager::getInstance()->getCurrentPool()->addObject(this);
return this;
} //获取引用计数值
unsigned int Ref::getReferenceCount() const
{
return _referenceCount;
}

1.2 AutoreleasePool

// 存放释放池对象的数组
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;
}

1.3 PoolManager

// 释放池管理器单例对象
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();
}

1.4 DisplayLinkDirector

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

根据目前的分析,我们先来捋一捋,待会儿再进一步深入。内存管理的过程是怎么样的呢?首先,创建了一个 Node 对象A,Node 继承Ref,因此 Ref 的引用计数为1;然后,A通过 autorelease 将自己放入自动释放池;drawScene() 完成后,一帧结束,Director 通过释放池将池中的对象 clear(),即对 Node 对象A进行 release() 操作。A的引用计数变为0,执行 delete 释放A对象。

接下来我们继续介绍另外几个与内存管理有关的类。

2、第二部分

Node 类:提供了 addChildremoveChild 方法来创建游戏的节点树。

Vector 类:封装了对于对象的 retain 操作和 release 操作。

2.1 Node

// 添加节点
void Node::addChild(Node *child)
{
CCASSERT( child != nullptr, "Argument must be non-nil");
this->addChild(child, child->_localZOrder, child->_name); // 经过这个方法-->addChildHelper-->insertChild,完成retain操作
} // 移除节点
void Node::removeChild(Node* child, bool cleanup /* = true */)
{
//
if (_children.empty())
{
return;
} //
ssize_t index = _children.getIndex(child);
if( index != CC_INVALID_INDEX )
this->detachChild( child, index, cleanup );//注意这个函数
} // 插入节点
void Node::insertChild(Node* child, int z)
{
_transformUpdated = true;
_reorderChildDirty = true;
_children.pushBack(child);// pushBack方法对节点进行了retain
child->_setLocalZOrder(z);
} // 剥离节点
void Node::detachChild(Node *child, ssize_t childIndex, bool doCleanup)
{ ...// 部分省略 _children.erase(childIndex);// erase方法对节点进行了release
}

2.2 Vector

// 这里仅展示与Node类相关的内存管理的部分

// 将对象入栈,引用+1
void pushBack(T object)
{
CCASSERT(object != nullptr, "The object should not be nullptr");
_data.push_back( object ); object->retain(); // 进行了retain
} // 将目标位置的对象移除
iterator erase(ssize_t index)
{
CCASSERT(!_data.empty() && index >=0 && index < size(), "Invalid index!");
auto it = std::next( begin(), index ); (*it)->release(); // 进行了release return _data.erase(it);
}

到这里,终于可以把故事完整地讲一遍了。内存管理的过程是怎么样的呢?首先,创建了一个 Node 对象A, Node 继承 Ref,因此 Ref 的引用计数为1;然后A又通过 autorelease 将自己放入自动释放池;接着,有个 Node 对象B,B通过 addChild(A) 使得A的引用+1;几个 mainLoop 后,B通过 removeChild(A) 使得A的引用-1;这个 mainLoopdrawScene() 完成后,一帧结束, Director 通过释放池将池中的对象 clear(),即对 Node 对象A进行 release 操作。A的引用计数变为0,执行 delete 释放A对象。

3、高阶用法

之所以称为高阶用法,是因为,如果开发者对 Cocos 的内存管理机制理解不够深刻,那么很可能会用错而导致损失大于收益。另一方面,这类用法在平时很少会用到。

3.1 使用 retain 来延长对象的生存时间

在开发过程中,如果需要使用一个节点对象,但是又不想把它放到节点树里面去。那么就可以使用 retain 来避免对象被自动释放掉。

3.2 使用 PoolManagerpush 来延长对象的生存时间

有些情况下,希望闲置对象晚一帧进行销毁,可以使用 push 把当前释放池推入栈底,那么这一帧结束的时候只会释放刚 push 进去的释放池。

笔者本身还没有机会使用过高阶用法,如果有小伙伴发现了高阶用法在实际问题中的应用,敬请留言交流。

四、参考链接

深入理解 Cocos2d-x 内存管理 (从这篇文章中获得许多启发。)

Cocos2d-x源码(位于Github上,如果连不上尝试使用前缀 https)

引用计数——维基百科(关于引用计数的说明)

Cocos2d-x的内存管理机制概述(里面提到了为什么要有 PoolManager)

cocos2dx 3.2 (24)——内存管理机制(编纂地比较详细)

Cocos2d-x 3.2 的内存管理详解的更多相关文章

  1. Apache Spark 内存管理详解(转载)

    Spark 作为一个基于内存的分布式计算引擎,其内存管理模块在整个系统中扮演着非常重要的角色.理解 Spark 内存管理的基本原理,有助于更好地开发 Spark 应用程序和进行性能调优.本文旨在梳理出 ...

  2. 动态内存管理详解:malloc/free/new/delete/brk/mmap

    c++ 内存获取和释放 new/delete,new[]/delete[] c 内存获取和释放 malloc/free, calloc/realloc 上述8个函数/操作符是c/c++语言里常用来做动 ...

  3. spark内存管理详解

    Spark 作为一个基于内存的分布式计算引擎,其内存管理模块在整个系统中扮演着非常重要的角色.理解 Spark 内存管理的基本原理,有助于更好地开发 Spark 应用程序和进行性能调优.本文旨在梳理出 ...

  4. MemCache中的内存管理详解

    MC的内存管理机制 1.内存的碎片化 当我们使用C语言或者其他语言进行malloc(申请内存),free(释放内存)等类似的命令操作内存的时候, 在不断的申请和释放的过程中,形成了一些很小的内存片段, ...

  5. 转:C/C++内存管理详解 堆 栈

    http://chenqx.github.io/2014/09/25/Cpp-Memory-Management/ 内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了 ...

  6. QF——OC内存管理详解

    堆的内存管理: 我们所说的内存管理,其实就是堆的内存管理.因为栈的内存会自动回收,堆的内存需要我们手动回收. 栈中一般存储的是基本数据类型变量和指向对象的指针(对象的引用),而真实的对象存储在堆中.因 ...

  7. Swift 内存管理详解

    Swift内存管理: Swift 和 OC 用的都是ARC的内存管理机制,它们通过 ARC 可以很好的管理对象的回收,大部分的时候,程序猿无需关心 Swift 对象的回收. 注意: 只有引用类型变量所 ...

  8. Memcached 内存管理详解

    Memcached是一个高效的分布式内存cache,了解memcached的内存管理机制,便于我们理解memcached,让我们可以针对我们数据特点进行调优,让其更好的为我所用. 首先需要我们先了解两 ...

  9. IOS内存管理详解

    一.    基本原理 1.        什么是内存管理 移动设备的内存极其有限,每个app所能占用的内存是有限制的 当app所占用的内存较多时,系统会发出内存警告,这时得回收一些不需要再使用的内存空 ...

随机推荐

  1. art-template循环无法显示出数据

    art-template循环遍历无法显示数据原因 1.语法问题:循环语句导致的问题 2.插件问题: 用标准语法时循环数据如果引入第一个插件,会导致数据显示不出来只有引入第二个插件才可循环出数据 用原生 ...

  2. dapr微服务.netcore sdk入门

    Actors入门 先决条件 .Net Core SDK 3.0 Dapr CLI Dapr DotNet SDK 概述 本文档描述如何在客户端应用程序上创建Actor(MyActor)并调用其方法. ...

  3. docker安装与配置

    Docker与虚拟化技术的区别 虚拟机分配多少宿主机就减少多少资源,比如VMware1分配了2Gb内存,如果运行5Gb的应用程序会造成内存溢出,vmware2分配了2Gb内存,如果运行2Gb的应用程序 ...

  4. Vmare 无法打开内核设备“\\.\VMCIDev\VMX”: 系统找不到指定的文件。您在安装 VMware Workstation 后是否进行了重新引导?的解决办法

    1.使用管理员省份运行cmd:net start vmx86(切记是要用管理员身份),启动服务成功问题即可解决. 2.若1操作中启动失败,则到Vmare安装目录下搜索vmx86.sys文件,并拷贝到C ...

  5. [ASP.NET Core 3框架揭秘] 跨平台开发体验: Windows [下篇]

    由于ASP.NET Core框架在本质上就是由服务器和中间件构建的消息处理管道,所以在它上面构建的应用开发框架都是建立在某种类型的中间件上,整个ASP.NET Core MVC开发框架就是建立在用来实 ...

  6. 微信小程序视图层介绍及用法

    一. 视图层 WXML(WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件.事件系统,可以构建出页面的结构. 1.1. 数据绑定 1.1.1. 普通写法 <vi ...

  7. 学习SQL注入---1

    开始接触SQL注入了,最开始根据网上的思路做了两道注入的题,但对于SQL注入如何实现,怎么一个流程还是不理解.后来,在网上查找了很多资料,现在一点点去理解. 1.利用sqlmap注入的时候,不是所有页 ...

  8. Gitlab + Jenkins 的 CI 实践

    0x00 事件 为了开发人员更高效的更新应用而采取的 CI 方式实践. 0x01 过程记录 1.Jenkins 设置 安装插件 Gitlab Hook Plugin Build Authorizati ...

  9. 剑指offer-39:平衡二叉树

    题目描述 输入一棵二叉树,判断该二叉树是否是平衡二叉树. 解题思路 在做这题是,我第一反应就是遍历两次二叉树.第一遍记录每个节点的深度,并将信息存入HashMap中,key = node,value ...

  10. Oracle11g在虚拟机win7上的详细安装过程(包括win7在虚拟机上的安装)

    http://www.imsdn.cn/这个是镜像文件的下载地址,之前下载雨林和深度的VM识别不了. 这个好了之后就可以去这个网址下看安装教程很详细.https://blog.csdn.net/u01 ...