Cocos2dx中的内存管理是极为重要的一部分,本章就将讲一讲内存管理机制。
本文仅供个人记录和复习,不用于其他用途
3.x版本的改变
3.x版本使用了全新的根类Ref
,不再使用2.x版本中的CCObject
。引擎中所有的类都派生自Ref
。
为什么要内存管理
C++中由于存在着指针,所以程序员可以使用指针来获取堆上的内存。相比于有限的栈上空间,堆上的空间要更大一些。但是呢,指针分配了内存之后,我们必须将这段内存释放掉,否则会出现内存泄露的情况。内存的分配与释放是非常麻烦的事情,稍不留神就会忘记。Cocos2dx中没有使用构造函数和析构函数,而是分别采用了自定义create()
方法以及引用计数的方式来控制内存的分配与释放。
引用计数
什么是引用计数呢?看看下面这张图你就会明白:
</center>
如果把房子看做一段内存,把人看作使用者的话,上图所示的步骤就可以这么解释:
- 一开始,这段内存放在堆上无人使用,引用计数为0
- 随后有一人进来,一般表示这段内存被分配给了一个指针,引用计数为1
- 又进来一个人使用,引用计数为2
- 同样的,再次被使用,引用计数为3
- 有一个人使用完了,那么引用计数为2
- 再次使用完,引用计数为1
- 最后,这个指针不需要了,引用计数为0,释放这段内存
也就是说,每当一段内存被使用时,引用计数就加1,每当结束一次使用,引用计数就减1。直到引用计数为0时,就将这段内存释放,达到了内存管理的效果。
主要方法
retain()
方法,使对象的引用计数加1,表示获取该对象的使用权release()
方法,使对象的引用计数减1,表示释放该对象的使用权autorelease()
方法,将对象放入自动回收池,自身被释放时对池中所有对象执行release()
方法,十分方便
关于自动回收池,Cocos定义了AutoreleasePool
类,用于管理自动回收对象。
Ref类
查看CCRef
文件,我们可以看到Ref类的实现,这里只摘抄了重要的代码:
|
|
Ref
类定义了_referenceCount
变量用于引用计数,构造函数将_referenceCount
初始化为1。retain()
将引用计数加1,release()
将引用计数减1。这里还有一个判断,那就是当引用计数为0时,release()
方法会将内存释放。autorelease()
用于将对象放入自动回收池。
自动回收池详解
|
|
控制台输出日志如下:
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的引用计数不变。随后我们执行一组retain()
和release()
,引用计数还是为1。但是呢,当这一帧结束(这里就是转换场景),自动回收池将对池中所有对象进行一次release()
。
从这里可以看出,autorelease()
是在一帧结束后才会释放。如果对象的释放次数超过了应有次数,这个错误不会被立马发现,只有当一帧结束时,游戏才会崩溃。比如,一个对象含有1个引用计数,但是被调用了两次autorelease()
。不过,游戏不会立马崩溃,而是会继续执行这一帧,直到结束时才会崩溃。所以,除非是工厂方法这种不得不用的情况,一般只是用release()
来释放对象。
AutoreleasePool类
虽然autorelease()
很方便,但我们要知道,自动回收池本身占用着内存和CPU。如果一帧之中产生大量的autorelease()
,回收池的性能会下降。所以,在autorelease()
产生的密集区域前后,我们最好手动创建并释放一个新的回收池。
|
|
autorelease()注意事项
对于autorelease()
,有以下几个要注意的地方:
autorelease()
的实质是将对象加入自动释放池,对象的引用计数不会立刻减1,在自动释放池被回收时对象执行release()
autorelease()
并不是毫无代价的,其背后的释放池机制同样需要占用内存和CPU资源- 不用的对象推荐使用
release()
来释放对象引用,立即回收
特殊的内存管理
工厂方法
Cocos2dx中提供了大量的工厂方法来创建对象,比如我们很熟悉的create()
方法:
|
|
我们可以看到,工厂方法之中用到了autorelease()
。
addChild()和removeChild()
在Cocos2d-x中,所有继承自Node类
,在调用addChild
方法添加子节点时,自动调用了retain
。对应的通过removeChild
,移除子节点时,自动调用了release
。
调用addChild方法添加子节点,节点对象执行retain
。子节点被加入到节点容器中,父节点销毁时,会销毁节点容器释放子节点。对子节点执行release
。如果想提前移除子节点我们可以调用removeChild
。
在Cocos2d-x内存管理中,大部分情况下我们通过调用addChild
、removeChild
的方式自动完成了retain
,release
调用:
|
|
控制台输出如下:
retain count 1
retain count 2
可以看到,当我们使用工厂方法create()
创建spr
时,引用计数为1,随后使用addChild()
将spr
加入层,引用计数为2。那么呢,延迟2秒后,定时器调用mySchedule()
,将spr
从父节点上移除。我们在CCNode
中可以查看相关代码:
|
|
我们可以看到,当调用removeFromParent()
时,实际上就是将cleanUp
设置为true
,然后在removeFromParentAndCleanup()
方法中,让父节点调用方法removeChild()
。removeChild()
中根据子节点的编号,调用detachChild()
方法,将子节点退出,并且把父节点设置为空,同时进行其他的擦除动作。
至于cleanUp()
方法,便是用于子节点的清理:
|
|
事实上,当子节点被移除时,便退出了渲染树。可以说,如果精灵没有加入渲染树,那么便毫无用处。
内存优化
内存优化原理
游戏中最耗费内存的无疑就是纹理,因此,我们应该想办法减纹理内存的使用,否则游戏的运行会很不流畅。但是呢,也不能过度优化,因为游戏的画质和游戏的流畅运行本身就是难以取舍的。比如,我们在玩游戏的时候,我们可以通过设置选项来调节游戏的画质。我们知道,当游戏的画质调低时,游戏的画面会变得模糊,特别是当近距离观看时,物体的边缘锯齿会很明显。又或者是物体的色彩不再鲜艳,而是换成较为稀薄的颜色。
内存优化注意事项
- 一帧一帧载入游戏资源
- 减少绘制调用,使用“Auto-batching”自动批处理
- 载入纹理时按照从大到小的顺序
- 避免高峰内存使用
- 使用载入屏幕预载入游戏资源
- 需要时释放空闲资源
- 收到内存警告后释放缓存资源
- 使用纹理打包器优化纹理大小、格式、颜色深度等
- 使用JPG格式要谨慎
- 请使用RGB4444颜色深度16位纹理
- 请使用NPOT纹理,不要使用POT纹理
- 避免载入超大纹理
- 推荐1024*1024 NPOT pvr.ccz纹理集,而不要采用RAW PNG纹理