Cocos内存管理源码(autorelease解析)

背景

这段时间在做项目的时候,需求需要往spine动作的挂点上绑定按钮节点,由于按钮在编辑器中是加在已有节点上的,所以在往spine上添加挂点时,需要先移除按钮,然后再绑定的挂点上。

local spineAnim = sp.SkeltonAnimation:create(skeletonFile, atlasFile, 1.0, true)
local btnGame = roleNode:getChildByName("btnGame")
btnGame:removeFromParent()
spineAnim:addSlotBindInfo("qianwangduiju", btnGame, Defind.slotBindType.slotBindType_all)

如果直接这样写,会在某种情况导致btnGame按钮节点丢失,无法正常挂载再spine动画节点上,后续优化了此方案

local spineAnim = sp.SkeltonAnimation:create(skeletonFile, atlasFile, 1.0, true)
local btnGame = roleNode:getChildByName("btnGame")
btnGame:retain()
btnGame:removeFromParent()
btnGame:autorelease()
spineAnim:addSlotBindInfo("qianwangduiju", btnGame, Defind.slotBindType.slotBindType_all)

在移除按钮之前,先retain一下,这样引用计数加1,就不会导致内存被回收,再调用autorelease,此时并不会release对象,这时会将此节点加入_managedObjectArry对象池中,在Director的mainLoop中会调用PoolManager::getInstance()->getCurrentPool()->clear();

具体详情解析,请接着看

cocos2dx-3.8中的自动内存管理是用引用计数来实现的,对于老版本的coocs引用计数使用的是CCObejct,但这个类后续被弃用了,使用CCRef代替,cocos中几乎所有的类都是继承于CCRef

CCRef基本原理就是其内部存在一个引用计数_referenceCount,当这个计数为0时,就会被释放。引用计数通过retain,release操作。

Ref从创建到销毁的过程

举个栗子,向屏幕中添加一个Button来测试Ref的创建和销毁,首先创建一个Button

auto button = Button::create();
button->setName("myButton");
addChild(button);

以上代码就是向屏幕中添加一个button,让我们看看create做了什么

Button* Button::create()
{
Button* widget = new (std::nothrow) Button();
if (widget && widget->init())
{
widget->autorelease();
return widget;
}
CC_SAFE_DELETE(widget);
return nullptr;
}

create函数是一个工厂方法,cocos中很多类都实现了这个方法,其中可以看到ret->autorelease();,这个函数就是把当前对象加入到自动释放池内,对于自动释放池下面会详细讲解。(注:Ref初始化的时候引用计数为1不是0

接下来看下addChild()接口,此处截取了一部分

void Node::addChild(Node *child){
CCASSERT( child != nullptr, "Argument must be non-nil");
this->addChild(child, child->_localZOrder, child->_name);
} void Node::addChild(Node* child, int localZOrder, const std::string &name){
...
addChildHelper(child, localZOrder, INVALID_TAG, name, false);
} void Node::addChildHelper(Node* child, int localZOrder, int tag, const std::string &name, bool setTag){
...
this->insertChild(child, localZOrder);
...
} void Node::insertChild(Node* child, int z){
_transformUpdated = true;
_reorderChildDirty = true;
_children.pushBack(child);
child->_localZOrder = z;
}

最终跳转到insertChild中,通过 _children.pushBackbutton加入到_children中去,到底引用计数在哪里+1操作?答案在pushBack操作中,_childrencocosRef量身定制的向量Vector<T>,这个向量只能给继承Ref的类使用

void pushBack(T object){
...
_data.push_back( object );
object->retain();
}

代码中可以看到object->retain(),对添加进来的对象引用+1操作,那么什么时候-1呢?

当我们移除场景的时候,应该释放场景中的button的。Node被移除时会调用当前的Node的父亲的removeChild函数,此函数最后会调用Nodecleanup函数,cleanup函数时递归函数,会遍历所有子节点。当cleanup完之后会从父节点的_children这个向量中删除,此时就会调用release函数

//当某个儿子节点cleanup完之后会调用_children.earse(childIndex)
iterator erase(ssize_t index){
...
auto it = std::next( begin(), index );
(*it)->release();
return _data.erase(it);
}

release函数就是当前实例的引用计数-1,如果-1后为0那么释放内存

void Ref::release(){
...
--_referenceCount; if (_referenceCount == 0){
...
delete this;
}
}

自动释放池

Ref中的autorelease函数,咋一看感觉内存不需要我来管了,他会自动释放。然而这个自动和我脑子里面的自动向差一个孙猴子的跟头,毕竟c++不是java,先看看autorelease的源码

Ref* Ref::autorelease()
{
PoolManager::getInstance()->getCurrentPool()->addObject(this);
return this;
}

代码很短,autorelease并没有release,而是把对象加入到了对象池中。那么这个对象池是什么时候去release里面的对象呢?接下来就要看DirectormainLoop函数了,这个函数在Director中实现。

void Director::mainLoop()
{
if(_purgeDirectorInNextLoop)
{
...
}
else if(_restartDirectorInNextLoop)
{
...
}
else if(!_invalid)
{
drawScene(); // release the objects
PoolManager::getInstance()->getCurrentPool()->clear();
}
}

mainLoop是每一帧调用的函数,我们发现cocos在每一帧结束绘制drawScene之后都会调用PoolManager::getInstance()->getCurrentPool()->clear();的操作,接下来我们看看clear的实现细节。

void AutoreleasePool::clear()
{
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
_isClearing = true;
#endif
std::vector<Ref*> releasings;
releasings.swap(_managedObjectArray);
for (const auto &obj : releasings)
{
obj->release();
}
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
_isClearing = false;
#endif
}

我们发现,在clear里面对所有在_managedObjectArray中的所有对象都进行一次release操作,并把它从_managedObjectArray中删掉。_managedObjectArray是什么,查看前一段代码中addObject的实现细节就知道,autorelease就是把当前对象加入到_managedObjectArray

也就是说,我们创建的Button的时候引用计数为1,然后调用autorelease添加到_managedObjectArray中,之后又被addChild到屏幕中,此时引用计数为2。当一帧绘制结束的时候会系统会调用释放池的clear函数,此函数会遍历所有在自动释放池内的对象并release,最后从对象池中删除之(所以第二帧结束后不会被再次调用release了),此时引用计数为1。当我们把当前场景移除的时候会调用release把引用计数减少至0,并从内存中释放。

Cocos内存管理解析 CCRef/retain/release/autorelease的更多相关文章

  1. objective-C 的内存管理之-自动释放池(autorelease pool)

    如果一个对象的生命周期显而易见,很容易就知道什么时候该new一个对象,什么时候不再需要使用,这种情况下,直接用手动的retain和release来判定其生死足矣.但是有些时候,想知道某个对象在什么时候 ...

  2. iOS 内存管理-copy、 retain、 assign 、readonly 、 readwrite、nonatomic、@property、@synthesize、@dynamic、IB_DESIGNABLE 、 IBInspectable、IBOutletCollection

    浅谈iOS内存管理机制 alloc,retain,copy,release,autorelease 1)使用@property配合@synthesize可以让编译器自动实现getter/setter方 ...

  3. C#内存管理解析

    前言:对于很多的C#程序员来说,经常会很少去关注其内存的释放,他们认为C#带有强大的垃圾回收机制,所有不愿意去考虑这方面的事情,其实不尽然,很多时候我们都需要考虑C#内存的管理问题,否则会很容易造成内 ...

  4. Linux内存管理解析(一) : 分段与分页机制

    背景 : 在此文章里会从分页分段机制去解析Linux内存管理系统如何工作的,由于Linux内存管理过于复杂而本人能力有限.会尽量将自己总结归纳的部分写清晰. 从实模式到保护模式的寻址方式的不同 : 1 ...

  5. Linux内存管理解析(二) : 关于Linux内存管理的大体框架

    什么是内存管理 ? 首先内存管理管理的主要对象是虚拟内存,但是虚拟内存对应的映射主要为物理内存,其次也可能通过交换空间把虚拟内存与硬盘映射起来,既然如此,那我们先了解物理内存的管理. 对于物理内存而言 ...

  6. 深入解析alloc/retain/release/dealloc实现

    首先通过GNUstep上得源码来叙述各个函数的实现(GNUstep是Cocoa框架的互换框架,二者的行为和实现方式很相似) GNUstep源码中NSObject类的alloc方法: id = obj ...

  7. linux内存管理解析1----linux物理,线性内存布局及页表的初始化

    主要议题: 1分页,分段模式及实模式 2Linux分页 3linux内存线性地址空间布局及物理内存空间布局 4linux页表初始化及代码解析 1.1.1内存寻址和保护模式 在X86平台上,内存控制单元 ...

  8. Linux内存管理解析(三) : 内核对内核空间的内存管理

    内核采用 struct page 来表示一个物理页,在其中记载了诸多物理页的属性,比如 物理页被几个线程使用(如若没有则表示该页可以释放),页对应的虚拟地址. 首先需要知道的是,分配物理页可以分为两个 ...

  9. WINDOWS页式内存管理解析

    jpg 改 rar

  10. iOS 进阶—— iOS 内存管理

    1 似乎每个人在学习 iOS 过程中都考虑过的问题 alloc retain release delloc 做了什么? autoreleasepool 是怎样实现的? __unsafe_unretai ...

随机推荐

  1. NAT模式LVS负载均衡集群

    NAT模式LVS负载均衡集群 负载调度器:内网 网关 ens33:192.168.1.200,外网 网关 ens36:12.0.0.10 Web节点服务器1:192.168.1.100 Web节点服务 ...

  2. Laf 云开发平台及其实现原理

    Laf 产品介绍 自我介绍 大家好,我是来自 Laf 团队的王子俊,很高兴今天能在这里给大家分享我们 Laf 云开发平台及其实现原理.本来想说一点什么天气之类的话作为开头,但主持人都说完啦,我就不多说 ...

  3. Go基础之变量和常量

    Go基础之变量和常量 目录 Go基础之变量和常量 一. 标识符.关键字.内置类型和函数 1.1 标识符 1.2 关键字 1.3 保留字 1.4 内置类型 1.4.1 值类型: 1.4.2 引用类型:( ...

  4. Full Tank 题解

    Full Tank 题目大意 给定一张 \(n\) 个点,\(m\) 条边的连通无向图,在每个点有一个加油站,油价为该点的点权,每条边的油耗为该边的边权.现给出若干询问,问一辆油箱容量为 \(c\) ...

  5. mysqli操作

    1.使用mysqli_connect()函数,语法如下: mysqli 对象名=mysqli_connect(数据库服务名,用户名,密码,数据库名) 例:$conn=mysqli_connect('l ...

  6. [论文研读]空天地一体化(SAGIN)的网络安全_A_Survey_on_Space-Air-Ground-Sea_Integrated_Network_Security_in_6G

    ------------恢复内容开始------------ 空天地一体化(SAGIN)的网络安全 目前关注的方面: 集中在安全通信.入侵检测.侧通道攻击.GPS欺骗攻击.网络窃听.消息修改/注入等方 ...

  7. 模拟退火算法(SA)

    求某个目标函数的最值 爬山法 首先我们通过爬山法来引出模拟退火算法 我们先看一个例子:求函数的最值 我们用爬山法解决这个问题的步骤 1.在解空间中随机生成一个初始解(图中小黄点就是我们生成的初始解) ...

  8. Android-Java-反序列化JSON

    import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; String jsonStr= WebAPIOperato ...

  9. 关于XML的总结——Schema和DTD(转)

    XML DTD(XML的文档类型定义)是近几年来XML技术领域所使用的最广泛的一种模式.但是,由于XML DTD并不能完全满足XML自动化处理的要求,例如不能很好实现应用程序不同模块间的相互协调,缺乏 ...

  10. Redis创始人开源最小聊天服务器,仅200行代码,几天功夫已获2.8K Star!

    中午时候,在技术交流群里聊起关于Redis创始人的一些趣事,比如离开Redis之后,去写科幻小说之类的. 因为好奇科幻小说,TJ君就去搜索了一下.结果一搜,发现Redis作者最近居然又搞了个新活儿! ...