首页 > 代码库 > cocos2dx 3.1从零学习(四)——内存管理(错误案例分析)
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。