Cocos2d-x 3.x基础学习:内存管理机制

2015年03月23日 11:40 0 点赞 0 评论 更新于 2025-11-21 18:14

在3.x版本中,Cocos2d-x采用全新的根类Ref来实现类对象的引用计数记录,引擎中的所有类均派生自Ref

1、引用计数

引用计数的概念可参考《维基百科》:引用计数。Cocos2d-x通过引用计数来管理内存,具体操作如下:

  • 调用retain()方法:使对象的引用计数加1,表示获取该对象的引用权。
  • 调用release()方法:在引用结束时,使对象的引用计数值减1,表示释放该对象的引用权。
  • 调用autorelease()方法:将对象放入自动释放池。当释放池自身被释放时,会对池中的所有对象执行一次release()方法,实现灵活的垃圾回收。Cocos2d-x提供了AutoreleasePool来管理自动释放对象。

核心类Ref实现了引用计数,以下是其代码定义:

// CCRef.h
class CC_DLL Ref
{
public:
void retain(); // 保留。引用计数+1
void release(); // 释放。引用计数-1
Ref* autorelease(); // 实现自动释放。
unsigned int getReferenceCount() const; // 被引用次数
protected:
Ref(); // 初始化
public:
virtual ~Ref(); // 析构
protected:
unsigned int _referenceCount; // 引用次数
friend class AutoreleasePool; // 自动释放池
};

// CCRef.cpp
// 节点被创建时,引用次数为 1
Ref::Ref() : _referenceCount(1)
{
}

void Ref::retain()
{
CCASSERT(_referenceCount > 0, "reference count should greater than 0");
++_referenceCount;
}

void Ref::release()
{
CCASSERT(_referenceCount > 0, "reference count should greater than 0");
--_referenceCount;
if (_referenceCount == 0)
{
delete this;
}
}

Ref* Ref::autorelease()
{
// 将节点加入自动释放池
PoolManager::getInstance()->getCurrentPool()->addObject(this);
return this;
}

Ref原理分析

  • 当一个Ref对象被初始化(即被new出来时),_referenceCount初始值为1。
  • 当调用该Ref对象的retain()方法时,_referenceCount加1。
  • 当调用该Ref对象的release()方法时,_referenceCount减1。
  • _referenceCount减为0,则删除该Ref对象。

2、retain()release()的使用

下面通过一个简单的例子来学习retain()release()的使用:

TestObject* obj1 = new TestObject("testobj1");
CCLOG("obj1 referenceCount=%d", obj1->getReferenceCount());
obj1->retain();
CCLOG("obj1 referenceCount=%d", obj1->getReferenceCount());
obj1->release();
CCLOG("obj1 referenceCount=%d", obj1->getReferenceCount());
obj1->release();

控制台显示的日志如下:

cocos2d: TestObject:testobj1 is created
cocos2d: obj1 referenceCount=1
cocos2d: obj1 referenceCount=2
cocos2d: obj1 referenceCount=1
cocos2d: TestObject:testobj1 is destroyed

通过上述例子和打印结果可以看出:

  • obj1对象创建后,引用计数为1。
  • 执行一次retain()后,引用计数变为2。
  • 执行一次release()后,引用计数回到1。
  • 再执行一次release()后,对象被释放。

因此,我们可以通过调用retain()方法获取对象的引用权,在引用结束时调用release()方法释放引用权,直到对象的引用计数为0时,对象被释放。

3、autorelease()的使用

同样通过一个简单的例子来学习autorelease()的使用,代码如下:

TestObject* obj = new TestObject("testobj");
CCLOG("obj referenceCount=%d", obj->getReferenceCount());
obj->autorelease();
CCLOG("obj is add in currentpool %s", PoolManager::getInstance()->getCurrentPool()->contains(obj) ? "true" : "false");
CCLOG("obj referenceCount=%d", obj->getReferenceCount());
obj->retain();
CCLOG("obj referenceCount=%d", obj->getReferenceCount());
obj->release();
CCLOG("obj referenceCount=%d", obj->getReferenceCount());
// obj in current pool will be release
Director::getInstance()->replaceScene(this);

控制台显示的日志如下:

cocos2d: TestObject:testobj is created
cocos2d: obj referenceCount=1
cocos2d: obj is add in currentpool true
cocos2d: obj referenceCount=1
cocos2d: obj referenceCount=2
cocos2d: obj referenceCount=1
...
cocos2d: TestObject:testobj is destroyed

通过代码和打印结果可知:

  • obj对象创建后,引用计数为1。
  • 执行一次autorelease()后,obj对象被加入到当前的自动释放池,但引用计数值并未减1。
  • 在下一帧开始前,当前的自动释放池会被回收,并对池中的所有对象执行一次release()操作。
  • 当对象的引用计数为0时,对象会被释放。
  • obj对象执行autorelease()后,又执行了一组retain()release()操作,此时引用计数仍为1。在场景切换后,当前的自动释放池被回收,obj对象执行一次release()操作后引用计数减为0,对象被释放。

需要注意的是,autorelease()只有在自动释放池被释放时才会进行一次释放操作。如果对象释放的次数超过了应有的次数,这个错误在调用autorelease()时不会被发现,只有当自动释放池被释放时(通常是游戏的每一帧结束时),游戏才会崩溃,此时定位错误会非常困难。例如,在游戏中一个对象含有1个引用计数,但被调用了两次autorelease(),第二次调用时游戏会继续执行这一帧,结束游戏时才会崩溃,很难及时找到出错的地点。因此,在开发过程中应避免滥用autorelease(),只在工厂方法等不得不用的情况下使用,尽量使用release()来释放对象引用。

4、AutoreleasePool类的使用

Cocos2d-x提供了AutoreleasePool来管理自动释放对象,下面通过一个简单的例子讲解其使用方法:

TestObject* obj2 = new TestObject("testobj2");
CCLOG("obj2 referenceCount=%d", obj2->getReferenceCount());
// use AutoreleasePool
{
AutoreleasePool pool;
obj2->retain();
CCLOG("obj2 referenceCount=%d", obj2->getReferenceCount());
obj2->release();
CCLOG("obj2 referenceCount=%d", obj2->getReferenceCount());
obj2->autorelease();
CCLOG("obj2 is add in pool %s", pool.contains(obj2) ? "true" : "false");
TestObject *obj3 = new TestObject("testobj3");
obj3->autorelease();
CCLOG("obj3 is add in pool %s", pool.contains(obj3) ? "true" : "false");
}

控制台输出日志如下:

cocos2d: TestObject:testobj2 is created
cocos2d: obj2 referenceCount=1
cocos2d: obj2 referenceCount=2
cocos2d: obj2 referenceCount=1
cocos2d: obj2 is add in pool true
cocos2d: TestObject:testobj3 is created
cocos2d: obj3 is add in pool true
cocos2d: TestObject:testobj2 is destroyed
cocos2d: TestObject:testobj3 is destroyed

通过代码和输出结果可以看到:

  • 创建obj2对象时,其引用计数为1。
  • 接着创建一个自动释放池,对obj2对象执行retain()release()操作后,再执行autorelease()操作,此时obj2对象被加入到当前新建的自动释放池。
  • 新建obj3对象并执行autorelease()操作,obj3也被加入到当前新建的自动释放池。
  • 在代码块结束后,自动释放池被回收,obj2obj3执行release()操作,引用计数减为0,对象被释放销毁。

我们可以自己创建AutoreleasePool来管理对象的autorelease操作。虽然Cocos2d-x保证每一帧结束后释放一次释放池,并在下一帧开始前创建一个新的释放池,但如果在一帧之内生成了大量的autorelease对象,会导致释放池性能下降。因此,在生成autorelease对象密集的区域(通常是循环中)的前后,最好手动创建并释放一个回收池,示例如下:

// example of using temple autorelease pool
{
AutoreleasePool pool2;
char name[20];
for (int i = 0; i < 100; ++i)
{
snprintf(name, 20, "object%d", i);
TestObject *tmpObj = new TestObject(name);
tmpObj->autorelease();
}
}

总结

  • autorelease()的实质是将对象加入自动释放池,对象的引用计数不会立刻减1,而是在自动释放池被回收时执行release()操作。
  • autorelease()并非毫无代价,其背后的释放池机制会占用内存和CPU资源。
  • 过多使用autorelease()会增加自动释放池的管理和维护成本,在内存和CPU资源本就不足的程序中会使系统资源更加紧张。
  • 此时需要合理创建自动释放池来管理对象的autorelease操作,对于不用的对象,推荐使用release()来释放引用,立即回收。

5、特殊内存管理

5.1、工厂方法create()

在Cocos2d-x中,提供了大量的工厂方法来创建对象,这些对象通常都是自动释放的。以Labelcreate方法为例:

Label* Label::create()
{
auto ret = new Label();
if (ret)
{
ret->autorelease();
}
return ret;
}

可以发现,创建Label对象后,对其执行了autorelease()操作,表示该对象是自动释放的。LayerSceneSprite等类的create()方法也类似。使用工厂方法创建对象时,虽然引用计数为1,但由于对象已被放入释放池,调用者没有对该对象的引用权,除非人为调用retain()来获取引用权,否则不需要主动释放对象。

5.2、NodeaddChild() / removeChild方法

在Cocos2d-x中,所有继承自Node类的对象,在调用addChild方法添加子节点时,会自动调用retain;通过removeChild移除子节点时,会自动调用release。调用addChild方法添加子节点时,节点对象执行retain,子节点被加入到节点容器中,父节点销毁时会销毁节点容器并释放子节点,对子节点执行release。如果想提前移除子节点,可以调用removeChild。在大部分情况下,通过调用addChild/removeChild方法可以自动完成retainrelease调用,无需再手动调用。

【内存优化】

1、内存优化原理

为优化应用内存使用,开发人员首先要了解什么最耗应用内存,答案是纹理,纹理几乎会占据90%的应用内存。因此,应尽量最小化应用的纹理内存使用,否则应用可能会因低内存而崩溃。本节介绍Cocos2d-x游戏通用的两条内存优化原理。

1.1、认识瓶颈寻找方案

要了解什么样的纹理最耗应用内存以及这些纹理会消耗多少内存,可使用苹果的工具“Allocation & Leaks”。在Xcode中长按“Run”命令,选择“Profile”即可启动这两个工具。使用Allocation工具可以监控应用的内存使用,使用Leaks工具可以观察内存的泄漏情况。此外,还可以通过一些代码获取游戏内存使用的其他信息,示例如下:

Sprite* bg = Sprite::create("HelloWorld.png");
bg->setPosition(240, 160);
this->addChild(bg);
CCLOG("%s", Director::getInstance()->getTextureCache()->getCachedTextureInfo().c_str());

调用上述代码后,游戏在DEBUG模式下运行,在Xcode控制台窗口会看到格式工整的日志信息,例如:

cocos2d: "****/HelloWorld.png" rc=2 id=3 480 x 320 @ 32 bpp => 600 KB
"/cc_fps_images" rc=5 id=2 999 x 54 @ 16 bpp => 105 KB
TextureCache dumpDebugInfo: 2 textures, for 705 KB (0.69 MB)

从日志中可以看到纹理的名称、引用计数、ID、大小及每像素的位数,最重要的是能显示内存的使用情况,如“cc_fps_images”消耗了105KB内存,“HelloWorld.png”消耗了600KB内存。

1.2、切勿过度优化

这是一个通用的优化规则。在优化过程中,需要进行权衡取舍,因为图像质量和图像内存使用往往是相互矛盾的,千万不要过度优化。

2、内存优化等级

将Cocos2d-x内存优化分为三个等级,每个等级的说明和策略不同。

2.1、客户端等级

这是最重要的优化等级,因为在Cocos2d-x引擎顶层编译游戏时,引擎自身会提供一些优化选项,在这个等级可以进行大部分优化,主要包括优化纹理、音频、字体及粒子的内存使用。

  • 纹理优化:影响纹理内存使用的主要因素有纹理格式(压缩还是非压缩)、颜色深度和大小。可以使用PVR格式纹理减少内存使用,推荐纹理格式为pvr.ccz。纹理使用的每种颜色位数越多,图像质量越好,但越耗内存,因此可以使用颜色深度为RGB4444的纹理代替RGB8888,可使内存消耗降低一半。此外,超大的纹理会导致内存相关问题,最好使用中等大小的纹理。
  • 音频优化:影响音频文件内存使用的因素有音频文件数据格式、比特率及采样率。推荐使用MP3数据格式的音频文件,因为Android平台和iOS平台均支持MP3格式,且MP3格式经过压缩和硬件加速。背景音乐文件大小应低于800KB,可通过减少背景音乐时间然后重复播放来实现。音频文件采样率大约在96 - 128kbps为佳,比特率44kHz即可。
  • 字体和粒子优化:使用BMFont字体显示游戏分数时,应尽可能使用最少数量的文字,例如只显示单位数的数字时,可以移除所有字母。对于粒子,可以通过减少粒子数来降低内存使用。

2.2、引擎等级

此等级需要OpenGL ES及游戏引擎高手。

2.3、C++语言等级

在这个等级中,建议编写无内存泄露的代码,遵循Cocos2d-x内置的内存管理原则,尽量避免内存泄露。

3、提示和技巧

  • 一帧一帧载入游戏资源。
  • 减少绘制调用,使用“Auto-batching”自动批处理。
  • 载入纹理时按照从大到小的顺序。
  • 避免高峰内存使用。
  • 使用载入屏幕预载入游戏资源。
  • 需要时释放空闲资源。
  • 收到内存警告后释放缓存资源。
  • 使用纹理打包器优化纹理大小、格式、颜色深度等。
  • 使用JPG格式要谨慎。
  • 请使用RGB4444颜色深度16位纹理。
  • 请使用NPOT纹理,不要使用POT纹理。
  • 避免载入超大纹理。
  • 推荐1024 * 1024 NPOT pvr.ccz纹理集,而不要采用RAW PNG纹理。

作者信息

boke

boke

共发布了 3994 篇文章