首页 > 代码库 > cocos2dx 3.1从零学习(四)——内存管理(错误案例分析)

cocos2dx 3.1从零学习(四)——内存管理(错误案例分析)

本篇内容文字比较较多,但是这些都是建立在前面三章写代码特别是传值的时候崩溃的基础上的。可能表达的跟正确的机制有出入,还请指正。 如果有不理解的可以联系我,大家可以讨论一下,共同学习。


首先明确一个事实,retainrelease是一一对应的,跟new和delete一样


1.引用计数retain release

这里请参考一下引用计数的书籍肯定说的比我讲的详细

简单一点理解就是new的指针加一个计数器每引用一次这块内存计数就加1。在析构的时候减1,如果等于0的时候就delete这个指针并置空

 

2.自动释放autolease

autorelease后的对象默认计数是1,并且autorelease的对象会被放到自动释放池里自动释放池这里有一个需要注意的地方自动释放池存储了当前帧所有的autorelease的对象在帧结束时对其中所有对象release一次处理完后这个释放池就不再拥有对这些对象的处理权也就是说自动释放池只会最其中的对象进行一次release操作释放的同时使用一个新的释放池存储后一帧定义的autorelease对象,如此循环下去。

 

精灵们create函数执行后会被放到自动释放池释放池会在每帧结束的时候调用对于引用计数为1的内存进行释放如果没有其他操作比如retain或者addchild的话那么引用计数没有增加当前帧结束后计数减10这个指针也就不复存在了


什么时候计数会加1

手动调用retain使引用技术加1;

cocos2dx我所见过的create静态方法都是调用autorelease计数默认为1。

每引用一次比如使用频率最多的addChild()会使其引用技术加1。


什么时候计数会减1?

手动调用release使引用技术减1;

自动释放池里的会在当前帧结束的时候减1。注意是当前帧后面的释放池里存储的是后面帧运行时定义的autorelease对象

如果一个场景析构会对所有的子节点release一次,这被称为链式反应

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

 

错误案例

我们在create如果不使用retain使引用计数加1的话,那么自动释放池会使其引用计数减1,如果在回调函数中使用addchild(sp)会崩溃

要想解决这个问题create后添加使用sp->retain();来增加它的引用计数

 

如下

     auto temp = Sprite::create("CloseNormal.png");
    temp->retain();//如果注释掉会崩溃。
    auto item4 =MenuItemLabel::create(Label::createWithBMFont("fonts/futura-48.fnt","Hell"),         [=](Ref * ref){
        addChild(temp);
    });

有些人可能会使用引用lambda表达式如下

    auto  temp =Sprite::create("CloseNormal.png");
    temp->retain();
    auto item4 =MenuItemLabel::create(Label::createWithBMFont("fonts/futura-48.fnt","Hell"),         [&](Ref * ref){
        addChild(temp);
    });

崩溃了引用的话 即使retain也会崩溃这个为什么呢

引用的话我们使用的是temp的别名引用也就指向指针的指针temp。当这个函数执行完的时候temp做为局部变量就会被释放所以我们在回调函数中使用的temp已经不存在了 如果是=赋值的话精灵的指针会拷贝一份传到lambda表达式中所以不会崩溃

 

要想解决引用崩溃的问题我们只要使temp不会被释放就好所以定义为成员变量可以解决引用的lambda表达式造成的问题大家可以尝试一下


深入理解CC_SYNTHESIZE_RETAIN

假装我们从未学习过CC_SYNTHESIZE_RETAIN第二篇讲过场景之间的正向传值如果我们在主场景create一个精灵然后赋值给下一个场景的成员变量Sprite *sp,对于这种autorelease的变量我们应该怎么进行传值操作呢

autorelease变量会在每一帧结束的时候计数减1进行销毁所以我们应该对其计数加1,避免下个场景使用的时候已经被删除


我们应该在主场景切换场景的时候这样写

voidMainScene::Morning_0623_MemoryManage(cocos2d::Ref * ref)
{
    auto scene = MemoryManage::createScene();
    auto memLayer = (MemoryManage *)scene->getChildren().at(0);
    tmpSp =Sprite::create("coc/buildings_lowres/59.0.png");//注意斜杠的方向
    tmpSp->retain();//引用计数加1,否则当前帧结束会被销毁
    memLayer->sp = tmpSp;//如果不retain的话会被自动释放掉   在切换场景的时候会被释放掉。
   Director::getInstance()->pushScene(scene);
}

在下个场景MemoryManage定义成员变量sp的时候应该对其进行初始化因为它是一个指针

我们应该定义Sprite *sp = nullptr;

否则在MainScene复制的时候会崩溃因为它的一个未知的指针指向了内存中未知的区域

崩溃的地方如下

断言失败    CCASSERT(_referenceCount > 0,"reference count should greater than 0");

因为这个时候sp是一个未知的指针


下面我们对主场景中

 tmpSp =Sprite::create("coc/buildings_lowres/59.0.png");创建的精灵的整个生命周期的引用计数进行分析


主场景createautorelease(1)->retain(2)->autorelease自动释放池release(1)->在子场景中被addchild(2)->子场景析构的链式反应(1)->???

请看子场景析构的时候计数还是1,这会造成内存泄露所以我们应该在析构函数中执行一次sp->release().手动减1。

 

CC_SYNTHESIZE_RETAIN的出现就是为了解决上述问题它只是把retainrelease操作包装了一下

这个时候你再去看一遍CC_SYNTHESIZE_RETAIN的源码

#defineCC_SYNTHESIZE_RETAIN(varType, varName, funName)    private: varTypevarName; public: virtualvarType get##funName(void) const { return varName; } public: virtual voidset##funName(varType var)   {     if (varName != var)     {         CC_SAFE_RETAIN(var);         CC_SAFE_RELEASE(varName);         varName = var;     } }

 

调用CC_SYNTHESIZE_RETAIN来给成员变量赋值时会对原来的变量进行一次retain操作然后需要我们在析构函数的时候添加对应的 CC_SAFE_RELEASE(varName);

 

现在说一下为什么在CC_SYNTHESIZE_RETAIN中对成员变量varName执行CC_SAFE_RELEASE(varName);

varName如果被不同的变量多次赋值会怎么样 每一次的赋值原来的变量都要做一次retain操作如果我们直接改变了varName的值而不改变它原来指向的内存的引用计数的话那么就会造成内存泄露 所以每次赋值都会对原来的内存进行一次release。

 

 

总结:retainrelease是一一对应的但是我们应该使用它们的加强版宏定义CC_SAFE_RETAINCC_SAFE_RELEASE。这两个可不是一一对应的比如我们 CC_SYNTHESIZE_RETAIN定义的变量只在析构函数中加一句CC_SAFE_RELEASE。