cocos2dx 3.1从零学习(四)——内存管理(错误案例分析)
本篇内容文字比較较多,可是这些都是建立在前面三章写代码特别是传值的时候崩溃的基础上的。可能表达的跟正确的机制有出入,还请指正。 假设有不理解的能够联系我。大家能够讨论一下,共同学习。
首先明白一个事实,retain和release是一一相应的,跟new和delete一样。
1.引用计数retain release
这里请參考一下引用计数的书籍,肯定说的比我讲的具体。
简单一点理解就是,对new的指针加一个计数器,每引用一次这块内存。计数就加1。
在析构的时候减1。假设等于0的时候就delete这个指针并置空。
2.自己主动释放autolease
autorelease后的对象默认计数是1。而且autorelease的对象会被放到自己主动释放池里。自己主动释放池这里有一个须要注意的地方,自己主动释放池存储了当前帧全部的autorelease的对象,在帧结束时对当中全部对象release一次。处理完后这个释放池就不再拥有对这些对象的处理权,也就是说自己主动释放池仅仅会最当中的对象进行一次release操作。
释放的同一时候使用一个新的释放池存储后一帧定义的autorelease对象。如此循环下去。
精灵们create函数运行后会被放到自己主动释放池,释放池会在每帧结束的时候调用,对于引用计数为1的内存进行释放。假设没有其它操作比方retain或者addchild的话,那么引用计数没有添加。当前帧结束后计数减1为0后。这个指针也就不复存在了。
什么时候计数会加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");创建的精灵的整个生命周期的引用计数进行分析。
主场景create时autorelease(1)->retain(2)->autorelease自己主动释放池release(1)->在子场景中被addchild(2)->子场景析构的链式反应(1)->???
请看子场景析构的时候计数还是1,这会造成内存泄露。所以我们应该在析构函数中运行一次sp->release().手动减1。
CC_SYNTHESIZE_RETAIN的出现就是为了解决上述问题,它仅仅是把retain和release操作包装了一下。
这个时候你再去看一遍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。
总结:retain和release是一一相应的,可是我们应该使用它们的加强版。宏定义CC_SAFE_RETAIN和CC_SAFE_RELEASE。这两个可不是一一相应的。
比方我们 CC_SYNTHESIZE_RETAIN定义的变量,仅仅在析构函数中加一句CC_SAFE_RELEASE。