手机游戏的开发中,控制内存是一个很重要的课题。由于机型之间的性能存在差异,很多时候需要处理内存方面的优化。一方面是代码中的优化,另一方面是图片渲染等方面的优化。
1、内存管理原则:
Cocos2D-x采用c++语言进行开发,C++中的对象采用new/delete机制进行管理,即当创建一个对象的时候,调用new来申请一部分内存,当不需要这个对象的时候,直接调用delete就可以释放这部分内存。这样处理的好处是程序可以完全掌握内存管理的方方面面,缺点是程序员有时会忘记释放内存,就会发生内存泄露,导致不可预计的后果。
Cocos2D-x采用引用计数的方式管理内存,基本原则是当创建一个新对象的时候,内存计数计为1,每次进行retain保留操作的时候,内存计数加1,每次进行release释放操作的时候,内存计数减1。另外,对一个对象进行自动释放对象操作autorelease表明这个对象处于自动管理的状态,会在内存管理池CCPoolManager中添加这个对象,并且在自动释放内存池CCAutoreleasePool的堆栈中申请一块内存池放入这个对象。之后在对象不被需要用的时候,引擎会自动清除它。引擎用单一的线程来进行场景的绘制,通过不断调用主循环这个函数,这个函数除了进行场景的绘制,也会调用CCPoolManager的pop函数对自动管理的对象进行释放操作,pop函数会对CCAutoreleasePool堆栈栈顶的内存池进行操作,将池内的对象标记为非自动管理状态,并进行一次release操作,清除引用计数为1的对象,然后取出前一个入栈的内存池,等待下一轮的释放。
注意:不推荐autorelease操作,因为这种机制是每帧检测一次,如果在某个对象上没有进行retain操作,很有可能在这一帧的时候就会被释放掉而释放掉是有用的内存,而如果进行了retain操作,释放的时候有可能会造成内存泄露。不仅如此,使用autorelease操作还会使得程序的执行效率下降,因此Cocos2D-x中并不推荐使用autorelease操作。Cocos2D-x中存在大量的静态工厂函数,这些函数全部使用了autorelease函数,通过静态工厂来生成对象可以简化代码。
内存管理有4个原则:
1、谁创建(create),谁释放。也就是说,创建是不需要retain,直接内存计数就是1,每次不需要的时候释放对象。
2、谁需要,谁保留并释放。当一个对象被其他指针需要的时候,该指针进行保留操作(retain),当不需要时进行release释放。
3、传递赋值时,先retain形参,后release原指针,最后赋值。
4、自动释放池CCPoolManager。将对象置于自动释放池中,每帧绘制结束就自动release池中的对象。
2、图片处理:
在IOS系统上,所有的图片的宽高都会自动处理成2的N次方,这就意味着一张宽1024像素、高1025像素大小的图片和宽1024像素、高2048像素的图片占用的内存是一样的。图片占用的内存可以由公式计算得到:长x宽x4。例如:一张宽和高都是1024像素的图片占用的内存大小是1024x1024x4B,也就是4MB。另外需要说明的是,IOS系统最大支持的图片尺寸是2048x2048。
在Cocos2D-x中进行图片加载时,如果第一次加载图片,就要把图片加载到缓存,然后从缓存中加载图片。如果缓存中已经有了,就直接从缓存中加载。图片的缓存有两种类型:一种是普通的图片加载缓存CCTextureCache,另一种是精灵帧缓存CCSpriteFrameCache,其中CCSpriteFrameCache加载的是一张拼接的大图,每个小图只是大图中的一个区域,这些区域信息都在plist文件中保存。用的时候只需要根据小图的名称,就可以通过plist配置文件找到相应图片,加载到这个区域。CCTextureCache是普通的图片缓存,所有直接加载的图片都会默认放到这个缓存中,以提高调用效率。因此,每次加载一张图片,或者通过plist加载一张拼接图时,都会将整张图片加载到内存中。如果不释放,就会一直占用着,因此需要相关的函数来清理不需要的内存,它的函数接口均是为了方便进行内存管理的,如:removeAllTextures()、removeUnusedTextures()等。
1 //载入贴图,定义回调函数
2 CCTextureCache::sharedTextureCache()->addImageAsync( "images/texture.png", this, callfuncO_selector(回调函数) );
注意:图片并非仅仅是读入的时候占用内存,而是在图片被渲染的时候,还会占用一定大小的内存。一般来说,一张图片占用的内存是它图片大小的2倍,但是对于已经占用渲染内存的图片,再次使用是不会占用更多的内存。
根据处理图片内存的方式,得出如下几条优化内存的方法:
1、在合适的时机释放内存
“Java时代”常见的缓解内存压力的方法是“何时使用何时加载,不使用时即释放”,比如:在场景切换(界面到游戏、游戏到界面、关卡到另一个关卡)的时候,将前一个场景的内存全部释放掉,一般要确保切换到下一场景之前将该场景初始化。如果从A场景切换到B场景,调用的函数顺序为B的init初始化函数,到A的exit退出函数,在到B的onEnter进入函数。可如果使用了场景间的切换效果,函数的调用顺序变为B的init初始化函数,到B的onEnter进入函数,再到A的exit退出函数。第二种方式会有一瞬间将两个场景的资源叠加在一起,可能会因为内存吃紧而崩溃,这时的处理方式是提前将前一个场景中不使用的资源释放。另外一种情况需要说明,那就是有时强制释放某些资源会导致正在执行的动作失去引用而出现异常,这时可以通过调用动作管理类的停止所有动作函数来释放节点的相关动作。
2、使用纹理贴图集的方法拼接图片
尽量使图片的边长保持在2的N次方,同时最好将有关系的图片(比如某个界面中的图片、某个游戏关卡中的图片)打包在一张大图里。这样一张图片的渲染内存只会使用一次,而且在这个场景中也只需要加载这一张图片就可以了,从而能够有效节约内存。
3、打包的时候要考虑到层的分布
有时为了渲染效率可能会用到CCSpriteBatchNode。同一个BatchNode里的图片都是位于一个层的,因此必须根据各个图片的层关系,打包到不同的plist文件里。
4、图片格式的选择
游戏中尽量不要使用JPG格式的图片,因为系统会把读入的JPG格式的文件转化为PNG格式。这样会造成两个问题:首先读入的速度会变慢,然后内存中会同时存在JPG和PNG两张相同的图片。因此在开发过程中,尽量不要使用JPG格式的图片。