从最初的:http://www.himigame.com/iphone-cocos2d/1043.html
译者:
在我完毕第一个游戏项目的时候。我深切地意识到“使用cocos2d来制作游戏的开发人员们,他们大多会被cocos2d的内存问题所困扰”。而我刚開始接触cocos2d的时候,社区里面的人们讨论了一个非常有意义的话题:“请简单地讲述你觉得新手cocos2d程序猿在他開始编码之前,最应该先知道,或者应该关注和注意的事项。”这个问题的答案非常多,有人讲是“怎样载入和保存游戏数据”,有人讲的是“怎样实现有限状态机”等等。而最吸引我的则是,有一个人讲到,新手cocos2d程序猿或者新手cocoa程序,他们所遇到的80%的问题都与内存相关。
由于有着c/c++背景的我。看到这句话的时候,非常是赞同,因此刚開始cocos2d编程的时候格外注意内存方面的问题。即便如此。在我完毕自己第一个游戏的过程中,还是遇到了大量的内存问题。它们让我头疼。让我睡不着觉。庆幸的是。我通过社区都找到了答案而且攻克了我的问题。
我在《我的第一个游戏FoodieTheBug完毕之后的几点心得体会》这篇博文中也讲述过一些内存方面的使用心得。
可是。不够详细,我当时想讲的内容有非常多。
由于有些难以用文字具象化,我也就偷了一回懒了。这次,当我看到Steffen Itterheim写了两篇这么经典的优化cocos2d内存使用和程序大小的文章之后,我有一种“于我心有戚戚焉”的感觉。我迫不及待地想跟大家分享。可惜非常多人抱怨说訪问不了,被墙了等等。
可能也有一些同行,对E文不是非常感冒。趁着周末,我花一个下午的时间。给大家翻译一下。与大家共勉。
全文例如以下:
我眼下正完毕我的最后一个合约项目。在这个项目的最后阶段,我须要考虑的一件事情就是怎样优化游戏的内存使用。
在今天的iDevBlogADay文章中,我将向大家讲述。我是怎样降低25-30MB游戏内存消耗的(如今游戏消耗内存90-95MB,我还通过这个过程,消除了一些因为内存警告而引起的程序崩溃问题)。
同一时候,我还将游戏程序的大小从25MB降低到了20MB下面(假设苹果没有在不久前将蜂窝网下载应用的限制从20MB提高到50MB的话,那么我这个小的优化就太棒了,它能够潜在地给我带来很多其它的下载量)。
我还会给大家介绍,怎样在你载入游戏资源的时候展示一个带有动画的Loading界面,我还会增加一些最佳实践和小技巧。
什么消耗了90%的内存?
大家猜一下:)
在大部分情况下,是纹理(textures)消耗了游戏程序大量的内存。
因此。纹理是我们首要考虑优化的对象,特别是当你碰到内存警告的问题的时候。
避免一个接一个地载入PNG和JPG纹理(他们之间至少等待一帧)
cocos2d里面纹理载入分为两个阶段:1.从图片文件里创建一个UIImage对象。2.以这个创建好的UIImage对象来创建CCTexture2D对象。这意味着,当一个纹理被载入的时候。在短时候内,它会消耗两倍于它本身内存占用的内存大小。(译注:为什么仅仅是短时间内呢?
由于autoRelease pool和引用计数的关系,暂时创建的UIImage对象会被回收。
)
当你在一个方法体内,接二连三地载入4个纹理的时候,这个内存问题会变得更加糟糕。由于在这种方法还没结束之前,每个纹理都会消耗两倍于它本身的内存。
我不是非常确定,如今的cocos2d是否仍然如此。或者这样的情况是否仅仅适用于手工引用计数管理,也许ARC不会如此呢?我习惯于按顺序载入纹理。可是在载入下一个纹理之前要等待一帧。这将会使得不论什么纹理载入的消耗对内存的压力降低。由于等待一帧,引用计数会把暂时的UIImage对象释放掉。降低内存压力。此外,在兴许的文章中。假设你想在背景线程中按序载入纹理的话,也能够採用这样的方法。
不要使用JPG图片!
cocos2d-iphone使用JPG纹理的时候有一个问题。由于JPG纹理在载入的时候,会实时地转化为PNG格式的纹理。这意味着cocos2d-iphone载入纹理是很慢的(这里有演示)。并且JPG纹理将消耗三倍于本身内存占用大小的内存。
一个2048*2048大小的纹理会消耗16M的内存。
当你载入它的时候。在短时间内,它将消耗32MB内存。如今。假设这个图片是JPG格式,你会看到这个数字会达到48MB,由于额外的UIImage对象的创建。
尽管,终于内存都会降到16M。可是,那一个时刻的内存飙高。足以让os杀死你的游戏进程,造成crash,影响用户体验。
JPG不论在载入速度和内存消耗方面都非常差。所以。千万不要使用JPG。
忽视文件图片大小
这样的情况,我见到非常多。它乍听起来可能认为有点荒诞,但事实如此。由于它须要关于文件格式的知识,而这些知识并非每个程序猿都了解的。我常常听到的论断就是“嘿!
我的程序不可能有内存警告,我全部的图片资源加起来还不到30MB!”。
怎么说呢,由于图片文件大小和纹理内存占用是两码事。
如果他们是帐篷。图片文件就相当于帐篷被装在行李箱。
可是。如果你想要使用帐篷的话,它必须被撑起来,被“膨胀”。
图片文件和纹理的关系与此类似。
图片文件大多是压缩过的,它们被使用的话必须先解压缩,然后才干会GPU所处理,变成我们熟知的纹理。一个2048*2048的png图片。採用32位颜色深度编码,那么它在磁盘上占用空间仅仅有2MB。
可是,假设变成纹理。它将消耗16MB的内存!
当然,降低纹理占用内存大小是有办法滴。
使用16-bit纹理
最高速地降低纹理内存占用的办法就是把它们作为16位颜色深度的纹理来载入。cocos2d默认的纹理像素格式是32位颜色深度。
假设把颜色深度减半,那么内存消耗也就能够降低一半。而且这还会带来渲染效率的提升。大约提高10%。
你能够使用CCTexture2D对象的类方法setDefaultAlphaPixelFormat来更改默认的纹理像素格式。代码例如以下:
[CCTexture2D setDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_RGB5A1]; [[CCTextureCache sharedTextureCache] addImage:@"ui.png"];
这里有个问题:首先。纹理像素格式的改变会影响后面载入的全部纹理。因此,假设你想后面载入纹理使用不同的像素格式的话,必须再调用此方法,而且又一次设置一遍像素格式。
其次,假设你的CCTexture2D设置的像素格式与图片本身的像素格式不匹配的话,就会导致显示严重失真。
比方颜色不正确,或者透明度不正确等等。
有哪些比較实用的纹理像素格式呢?
generate 32-bit textures: kCCTexture2DPixelFormat_RGBA8888 (default) generate 16-bit textures: kCCTexture2DPixelFormat_RGBA4444 generate 16-bit textures: kCCTexture2DPixelFormat_RGB5A1 generate 16-bit textures: kCCTexture2DPixelFormat_RGB565 (no alpha)
RGBA8888是默认的格式。
对于16位的纹理来说。使用RGB565能够获得最佳颜色质量,由于16位所实用来显示颜色:总共同拥有65536总颜色值。
可是,这里有个缺点,除非图片是矩形的,而且没有透明像素。所以RBG565格式比較适合背景图片和一些矩形的用户控件。
RBG5A1格式使用一位颜色来表示alpha通道。因此图片能够拥有透明区域。仅仅是,1位似乎有点不够用。它仅仅能表示32768种可用颜色值。并且图片要么仅仅能所有是透明像素,或者所有是不透明的像素。
由于一位的alpha通道的缘故,所以没有中间值。可是你能够使用fade in/out动作来改变纹理的opacity属性。
假设你的图片包括有半透明的区域,那么RBGA4444格式非常实用。它同意每个像素值有127个alpha值,因此透明效率与RGBA8888格式的纹理区别不是非常大。
可是。因为颜色总量降低至4096。所以,RBGA4444是16位图片格式里面颜色质量最差的。
如今,你能够得到16位纹理的不足之处了:它因为颜色总量的降低。有一些图片显示起来可能会失真,并且可能会产生“梯度”。
使16位纹理看起来更棒
幸运的是,我们有TexturePacker.(后面简称TP)
TP有一个特性叫做“抖动”,它能够使得原本因为颜色数量降低而产生的失真问题得到改善。(TP里面有非常多抖动算法。关于这些算法。读者能够參考我翻译的还有一篇文章)。
特别是在拥有Retina显示的像素密度下,你差点儿看不出16位与32位的纹理之间的显示区别。当然,前提是你须要採用“抖动”算法。
cocos2d默认的颜色深度将会把全部的纹理都渲染到16位的color framebuffer里面,然后再显示到你的设备屏幕上面。
既然这样。我们为什么不把全部的纹理的格式都弄成16位呢,32位又有什么用呢?反正它本来就会渲染到16位的framebuffer上去的。
这个问题有点太底层了。我不想深挖下去,并且我也不适合解释这个问题。(译者:哈哈,知之为知之,不知为不知)
使用NPOT纹理
NOPT是“non power of two”的缩写。译作“不是2的幂”。NPOT stands for “non power of two”. 在cocos2d1.x的时候,你必须在ccConfig.h文件里开启对NPOT的支持,可是,cocos2d 2.x就不须要了。它默认是支持NPOT的。全部3代(iphone 3GS)以后的ios设置都支持cocos2d 2.x(由于它们支持OpenGL ES2.0),所以也都能支持NPOT纹理。
假设纹理图集(texture atlas)使用NPOT的纹理,它将有一个具大的优势:它同意TP更好地压缩纹理。因此。我们会更少地浪费纹理图集的空白区域。
并且。这种纹理在载入的时候。会少使用1%到49%左右的内存。并且你能够使用TP强制生成NPOT的纹理。(你仅仅须要勾选“allow free size”就可以)
为什么要关心NPOT呢?由于苹果的OpenGL驱动有一个bug。导致假设使用POT的纹理,则会产生额外33%的内存消耗。
默认使用PVR格式的纹理
TP让你能够创建PVR格式的纹理。
除了PVR纹理支持NPOT外,它们不仅能够不是2的幂,并且还能够不是方形的。
PVR是最灵活的纹理文件格式。
除了支持标准的未压缩的RGB图片格式外。不支持有损压缩的pvrtc格式。另外。未压缩的pvr格式的纹理的内存消耗很地低。不像png图片那样要消耗2倍于本身内存占用大小的内存,pvr格式仅仅须要消耗纹理本身内存大小再加上一点点处理该图片格式的内存大小。
pvr格式的一个缺点就是。你不能在Mac上面打开查看。可是,假设你安装了TP的话,就能够使用TP自带的pvr图片浏览器来浏览pvr格式的图片了。
(强烈建议大家购买TP。支持TP,不要再盗版了)
使用PVR格式的文件差点儿没有缺点。
此外,它还能够极大地提高载入速度,后面我会解释到。
使用pvr.ccz文件格式
在三种可选用的pvr文件格式中,优先选择pvr.ccz格式。它是专门为cocos2d和TP设计的。在TP里面。这是它生成的最小的pvr文件。
并且pvr.ccz格式比其他不论什么文件格式的载入速度都要快。
当在cocos2d里面使用pvr格式的纹理时,仅仅使用pvr.ccz格式,不要使用其他格式。由于它载入速度超快,并且载入的时候使用更少的内存!
当视觉察觉不出来的时候。能够考虑使用PVRTC压缩
PVR纹理支持PVRTC纹理压缩格式。
它主要是採用的有损压缩。假设拿PVRTC图片与JPG图片作对照的话。它仅仅有JPG图片中等质量,可是,最大的优点是能够不用在内存里面解压缩纹理。
这里把32位的png图片(左边)与最佳质量的PVRTC4(4位)图片(点击图片查看完整的大小)作对照:
注意,在一些高对照度的地方,明显有一些瑕疵。有颜色梯度的地方看起来还好一点。
PVRTC肯定不是大部分游戏想要採用的纹理格式。可是,它们对于粒子效果来说。非常适用。由于那些小的粒子在不停地移动、旋转、缩放,所以你非常难看出一些视觉瑕疵。
PVRTC压缩图片格式
TP提供的PVR格式不仅有上面两种。还包含TC2和TC4这两种没有alpha通道的格式。
这里的alpha和16位纹理的alpha是一样的。没有alpha通道意味着图片里面没有透明像素,可是,很多其它的颜色位会用来表示颜色,那么颜色质量看起来也会更好一些。
有时候,PVRTC图片格式指的是使用4位或者2位颜色值 ,可是,并不全然是那样。
PVRTC图片格式能够编码很多其它的颜色值。
预先载入全部的纹理
就像标题所说,尽你所能。一定要预先载入全部的纹理。假设你的全部的纹理加起来不超过80MB内存消耗的话(指的是拥有Retina显示的设备,非Retina的减半考虑),你能够在第一个loading场景的时候就全部载入进来。
这样做最大的优点在于。你的游戏体验会表现得很平滑,并且你不须要再操心资源的载入和卸载问题了。
这样也使得你能够让每个纹理都使用合适的纹理像素格式,并且能够更方便地找出其他与纹理无关的内存问题。由于假设与纹理有关,那么在第一次载入全部的纹理的时候。这个问题就会暴露出来的。假设全部的纹理都载入完成,这时候再出现内存问题,那么肯定就与纹理无关了。而是其他的问题了。
假设你知道问题与纹理无关的话。那么你查找剩下的内存问题将会变得更加简单。并且你避免了前面说的这样的情况:当2048*2048的纹理载入的时候,它本来仅仅须要消耗16MB内存。可是短时间会冲到32MB内存。后面会提出一种方法来解决“间歇性内存飙高”(“译者发明滴”)的方法。(译者:希望下次开发人员的对话中“间歇性内存飙高”的说法会出现,呵呵)
依照纹理size从大到小的顺序载入纹理
因为载入纹理时额外的内存消耗问题,所以,採用按纹理size从大到小的方式来载入纹理是一个最佳实践。
如果,你有一个占内存16MB的纹理和四个占用内存4MB的纹理。如果你首先载入4MB的纹理。这个程序将会使用16MB的内存,而当它载入第四张纹理的时候,短时间内会飙到20MB。
这时。你要载入16MB的那个纹理了。内存会立即飙到48MB(4*4 + 16*2),然后再降到32MB(4*4 + 16)。
可是。反过来,你先载入16MB的纹理,然后短时候内飙到32MB。然后又降到16MB。这时候,你再依次载入剩下的4个4MB的,这时,最多会彪到(4*3 + 4*2 + 16=36)MB。
在这两种情况下。内存的峰值使用相差12MB,要知道,可能就是这12MB会断送你的游戏进程的小命哦!
避免在收到内存警告消息的时候清除缓存
我有时候看到了一种奇怪的“自己开枪打自己的脚”的行为:纹理已经所有在Loading场景里面载入完成了。这时候。内存警告发生了,然后cocos2d就会把没有使用的纹理从缓存中释放掉。
听起来不错,没有使用到的纹理都被释放掉了,可是!
。
。。
你刚刚把全部的纹理都载入进来,还没有进入不论什么一个场景中(此时全部的纹理都被当作“unused”)。可是立即被全部从texture cache中移除出去。
可是,你又须要在其他场景中使用它们。怎么办?你须要接着推断。假设有纹理没有载入。就继续载入。可是。一载入。因为“间歇性内存飙高”,又立即收到了内存警告。再释放。再推断。再载入。。。。 我的天,这是一个死循环啊。这也能解释为什么有些童鞋。在loading场景完了之后进入下一个场景 的时候非常卡的原因了。
如今,当我收到内存警告的时候。我的做法是----什么也不做。内存警告仍然在发生,可是,它仅仅是在程序刚開始载入的时候。
我知道这是为什么,由于“间歇性内存飙高”嘛,所以,我不去管它。(可是,假设是游戏过程中再收到内存警告。你就要注意了,由于这时候可能你有内存泄漏了!
!。)
我有时候会想办法改善一下,通过移除掉一些不使用的纹理和一些仅仅有在非常特殊的场景才会使用的图片(比方settings界面,玩家是不常常訪问的)。然后,无论什么时候,当我须要某张图片的时候,我会首先检查一下该sprite frame是否在cache中,假设没有就载入。你会在后面看到详细的做法。
理解在什么时候、在哪里去清除缓存
不要随机清除缓存,也能够心想着释放一些内存而去移除没有使用的纹理。那不是好的代码设计。有时候。它甚至会添加载入次数,并多次引发“间歇内存飙高”。
分析你的程序的内存使用,看看内存里面究竟有什么,以及什么应该被清除。然后仅仅清除该清除的。
你能够使用dumpCachedTextureInfo方法来观察哪些纹理被缓存了:
[[CCTextureCache sharedTextureCache] dumpCachedTextureInfo];
这种方法的输出例如以下:(为了清楚起见,我把那些与-hd后缀有关的信息屏蔽掉了)
cocos2d: "ingamescorefont.png" rc=9 name=ingamescorefont-hd.png id=13 128 x 64 @ 32 bpp => 32 KB cocos2d: "ui.png" rc=15 name=ui-hd.png id=5 2048 x 2048 @ 16 bpp => 8192 KB cocos2d: "ui-ingame.png" rc=36 name=ui-ingame-hd.png id=8 1024 x 1024 @ 16 bpp => 2048 KB cocos2d: "digits.png" rc=13 name=digits-hd.png id=10 512 x 64 @ 16 bpp => 64 KB cocos2d: "hilfe.png" rc=27 name=hilfe-hd.png id=6 1024 x 2048 @ 32 bpp => 8192 KB cocos2d: "settings.png" rc=8 name=settings-hd.png id=9 1024 x 1024 @ 16 bpp => 2048 KB cocos2d: "blitz_kurz.png" rc=1 name=(null) id=12 50 x 50 @ 32 bpp => 9 KB cocos2d: "gameover.png" rc=8 name=gameover-hd.png id=7 1024 x 2048 @ 32 bpp => 8192 KB cocos2d: "home.png" rc=32 name=home-hd.png id=4 2048 x 2048 @ 16 bpp => 8192 KB cocos2d: "particleTexture.png" rc=2 name=(null) id=11 87 x 65 @ 32 bpp => 22 KB cocos2d: "stern.png" rc=2 name=(null) id=2 87 x 65 @ 32 bpp => 22 KB cocos2d: "clownmenu.png" rc=60 name=clownmenu-hd.png id=1 1024 x 2048 @ 32 bpp => 8192 KB cocos2d: CCTextureCache dumpDebugInfo: 13 textures using 60.1 MB (纹理总共占用的内存大小!!!)
上面包括了许多实用的信息。纹理的大小、颜色深度(bpp)和每个被缓存的纹理在内存中所占用大小等。
这里的“rc”代表纹理的“引用计数”。假设这个引用计数等于1或2的话,那么意味着,这个纹理当前可能不会须要使用了,此时,你能够放心地把它从纹理cache中移除出去。
你仅仅移除你知道在当前场景下不太可能会被使用的纹理(即上面介绍的引用计数为1或2的情况),这是一个明智的做法。另外,仅仅移除那些占用内存大的纹理。假设一个纹理仅仅占几个kb的内存,其他移不移除都没什么太大的影响。(译注:这就和程序优化一样,不要做过多的细节优化,不要过早优化。要找到性能的瓶颈,然后再重点优化。以20%的时间换取80%的效率。过早和过多细节优化对于大多数程序而言,是须要极力避免的)。
SpriteFrames retain textures!
上面提到的样例中。纹理的引用计数可能有点让人看不懂。你会发现,纹理集有非常高的retain count。即使你知道这些纹理集中的纹理当前并没有被使用。
你可能忽略了一件事:CCSprteFrame会retain它的纹理。因此,假设你使用了纹理集,你要全然移除它不是那么easy。
由于,由这个纹理集产生的sprite frame还是保留在内存中。所以,你必须调用CCSpriteFrameCache的removeSpriteFramesFromTexture方法,能彻底清除纹理缓存中的纹理集。(译注:记住,不是你调用对象的release方法了,对象的内存就会被释放掉,而是引用计数为0了。内存才会被删除)
[[CCSpriteFrameCache sharedSpriteFrameCache] removeSpriteFramesFromTexture:uncachedTexture];
你也能够使用 removeSpriteFramesFromFile。并指定一个纹理集的.plist文件来清除缓存起来的精灵帧(spriteframes).
加入 SpriteFrames 很耗时, 每次都是!
Note: 这一点仅仅针对cocos2d v1.0有效。而cocos2d v2.x在载入之前会预先推断。
这样看起来有点无知(innocent):
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"ui.plist"];
可是,要注意。CCSpriteFrameCache并不会去检查一个精灵帧是否已经被缓存起来了。这与CCTextureCache的动作方式有所不同。它每次都会去载入spriteframes.
这个过程究竟须要耗费多少时间呢,这取决于你提供的.plist文件里精灵帧的数量。我注意到。仅仅有14帧的plist载入与有280帧的plist载入有着非常大的差别。
所以,对于精灵帧的载入,你也须要慎重。
所以。你要避免一些不必要的addSpriteFrames*方法调用。由于那边导致场景切换时产生小的卡顿。
你能够清除不论什么缓存(比方animation,sprite frames等),可是请不要轻易清除纹理缓存
cocos2d有很多缓存类。比方纹理缓存、精灵帧缓存,动画缓存等。可是,假设你想清理内存的话,精灵帧缓存和动画缓存对内存的占有是很少的。能够说是极少的。
当然,假设你想从内存中移除一个纹理,你也必须移除与之相关的精灵帧(由于精灵帧会retain纹理)。说白了,不要轻易去移除精灵帧和动画缓存。由于你有可能会使用到一个没有缓存的动画帧对象或者精灵帧对象,那样会导致程序crash。
例外:检查声音文件的内存使用!
声音文件会被缓存起来,然后能够反复播放而不会被中断。因为声音文件一般比較大。特别是,我看到有一些开发人员使用没有压缩的声音文件作为游戏的背景音乐,而这些背景音乐文件很大,它们一般会造成大量的内存消耗。
请使用MP3格式的声音文件。
由于使用没有压缩的声音文件既浪费内存又占用程序大小。当你载入完一些游戏音效时,在不须要的时候,记得要卸载掉。在第二篇文章中。我会向大家介绍有于声音文件很多其它的知识。
怎样避免缓存特定的纹理
假设你有一个纹理,你确实不想缓存起来。那怎么办呢?比方,在初始的载入场景中的图片,或者那些用户非常少会在意的图片--比方你的非常牛比的致谢场景的图片。
常常easy被误解的一点是,一个纹理显示出来了,那么它就被缓存起来了。假设你从缓存中移除此纹理,那么此时你再移除精灵就会程序崩溃。
这个理解不对。
CCTextureCache仅仅只是是对纹理再加入了一次retain函数的调用,这样。当没有其他对象(比方sprite)持有纹理的引用的时候,纹理仍然会存在内存之间。基于这一点。我们能够立刻从缓存中移除出去。这样,当纹理不存须要的时候。立即就会从内存中释放掉。例如以下代码所看到的:
bg = [CCSprite spriteWithFile:@"introBG.png"]; // don't cache this texture: [[CCTextureCache sharedTextureCache] removeTextureForKey:@"introBG.png"];
你须要记住,当你从CCTextureCache中移除一个纹理的时候,cocos2d下一次在调用spriteWithFile的时候,还是会再载入该纹理的--无论是否有没有一张名字一样的图片正在被其他精灵所使用。
因此,假设你不够细心的话。你有可能最后会在内存中载入两张反复的纹理。
有一个样例就是,当你在循环中载入纹理,而这些纹理你并不想缓存起来。这样的情况下,你就须要在循环之外去移除此纹理的缓存,否则可能会导致多个纹理被反复载入到内存之中:
NSArray* highscores = [Achievements sharedAchievements].highscores; for (HighscoreData* data in highscores) { NSString* entry = [NSString stringWithFormat:@"%05u", data.score]; CCLabelAtlas* label = [CCLabelAtlas labelWithString:entry charMapFile:@"pipizahlen.png" itemWidth:18 itemHeight:27 startCharMap:'.']; [labelsNode addChild:label z:10]; } // don't hold on to this texture: [[CCTextureCache sharedTextureCache] removeTextureForKey:@"pipizahlen.png"];
上面这个样例是我从highscore场景中抠出来的,一旦此场景退出,就不应该持有CCLabelAtlas纹理的引用。因此。我们须要把它从纹理缓存中移除出去。可是,你必须防止反复载入纹理到内存中去。
通过这样的方式,我们能够很方便地清除缓存中的纹理。并且最好是在创建纹理的时候清除,而不要在其他地方,比方dealloc或者索性让purge cache去做这个事。
使用一个Loading 场景
假设你不能预先载入全部的纹理的话。你能够使用一个loading场景,同一时候显示一个动画来表明载入的进度。
这样能够在进入下一个场景之前,让前面一个场景销毁,同一时候释放它所占用的内存资源。
实现起来很easy。
这个loading场景调度一个selector,然后每一帧(或者0.1秒也能够)运行一个函数。比方update。
除非你前面一个场景有内存泄漏。否则的话,每一次update函数运行的时候,都会把一些引用计数为0的内存资源释放掉。
在这个update方法里面,你能够创建新的场景。
这样极大地避免了“间歇性内存飙高”的问题,能够极大地减小内存压力。
在后台载入纹理
CCTextureCache类还支持异步载入资源的功能。利用addImageAsync方法。你能够非常方面地给addImageAsync方法加入一个回调方法,这样,当纹理异步载入结束的时候,能够得到通知。
这一点很重要:你必须等待一个资源载入完成。
否则的话,因为“间歇性内存飙高”,可能会引发下列问题:
1) 程序崩溃
2) 纹理被载入两次!
由于异步载入并不能保证载入顺序。
在后台载入其他游戏资源
但是。我们并没有方法来异步载入sprite frames和其他资源。但是,我们能够借助performSelectorInBackground来实现类似的异步载入的功能:
[self performSelectorInBackground:@selector(loadSpriteFrames:) withObject:nil];
里面的selector方法仅仅接收一个object參数(可是并没有使用)。然后就能够在此这方法里面异步载入资源了。例如以下所看到的:
-(void) loadSpriteFrames:(id)object { [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"hilfe.plist"]; [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"home.plist"]; [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"ui.plist"]; [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"gameover.plist"]; [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"ui-ingame.plist"]; [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"settings.plist"]; [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"digits.plist"]; }
这样做最大的优点在于。你载入资源的同一时候,loading场景还能够播放动画。能够加入精灵并执行一些action,这一切能够处理得非常平滑。这样的优势甚至在单个CPU的机器上面也表现得不错,可是假设你的设备有多个cpu的话效果更佳。
可是,你须要注意,你不能在后台线程载入纹理。你必须使用addImageAsync方法。
这是由于纹理必须与公共的OpenGL context在同样的线程中载入。这样,你就必须先异步载入纹理,然后再去后台载入sprite frames.你不能依靠CCSpriteFrameCache在后台线程中载入纹理。
按顺序载入游戏资源
以下的代码,是我採用的异步载入纹理和精灵帧的方法(在另外一个线程中载入:)
如果loadAssetsThenGotoMainMenu方法每一帧都会被触发。assetLoadCount和loadingAsset变量被声明在类接口中。分别 是init和bool类型:
-(void) increaseAssetLoadCount { assetLoadCount++; loadingAsset = NO; } -(void) loadAssetsThenGotoMainMenu:(ccTime)delta { NSLog(@"load assets %i", assetLoadCount); switch (assetLoadCount) { case 0: if (loadingAsset == NO) { loadingAsset = YES; NSLog(@"============= Loading home.png ==============="); [CCTexture2D setDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_RGB5A1]; [[CCTextureCache sharedTextureCache] addImageAsync:@"home.png" target:self selector:@selector(increaseAssetLoadCount)]; } break; case 1: if (loadingAsset == NO) { loadingAsset = YES; [self performSelectorInBackground:@selector(loadSpriteFrames:) withObject:nil]; } break; // extend with more sequentially numbered cases, as needed // the default case runs last, loads the next scene default: { [self unscheduleAllSelectors]; MainMenuScene* mainMenuScene = [MainMenuScene node]; [[CCDirector sharedDirector] replaceScene:mainMenuScene]; } break; } }
当这种方法执行到第一个case语句的时候,为了避免相同的图片被载入多次,我们把loadingAsset标记设置为yes。
当纹理载入完后,我们就加入increaseAssetLoadCount(这个数量能够用来显示运行进度条载入百分比)。后面的case语句还能够载入很多其他的其他东西,比方声音、字体文件、粒子效果、物理配置文件、关卡信息等。
无论载入多少东西。最后的default语句会执行,然后就能够进入MainMenuScene了。
这种方法的通用之处是。你能够通过case与assetLoadCount来异步载入多个纹理,同一时候又能避免“间歇性内存飙高”的问题。
由于每帧调用一次方法的时候。前面纹理载入多出来的暂时内存已经被释放掉了。由于当前线程栈顶的autoRelease pool会在每一帧渲染之前被清空。
后记:这里介绍的内容尽管是针对cocos2d-iphone的,可是,绝大部分内容是适合cocos2d-x的。因此。开发人员大可放心去试用这些方法。假设大家有更好的优化游戏内存使用的方法,欢迎分享。
希望此帖能成为cocos2d内存问题的终极解决方式帖。
假设大家认为我翻译的不错,希望能点一下旁边的推荐button。
Thanks, enjoy!:)
Happy coding!
怎样降低游戏程序大小?(曾经的目标是20MB下面。如今的目标是50MB下面,为什么?你懂的!)
降低你的程序的大小
把纹理的颜色位深度降低到16位。不仅能够降低内存压力。还能够有效地降低程序的体积。可是。我们还有其他方法能够更进一步地降低程序的大小。
TexturePacker PNG 图片优化
假设你有某些原因,让你坚持要使用PNG文件格式而不是我之前极力向你推荐的pvr.ccz文件格式,那么TexturePacker有一个选项。叫做“Png Opt Level”(Png优化级别)。能够帮助我们降低png文件的大小(注意:这样并不会影响图片载入时间)
就我眼下的理解来看,最大的优化级别能够生成最小的文件大小。可是。它有一个缺点。就是很耗时。对于2009年出的27寸的iMac来说,处理尺寸稍大的纹理,须要耗费10-20的时间来处理。
因为该优化过程採用了多线程的方式。所以。假设你有机器是四核的,那么速度应该会快一些。
当然,你仅仅有在真正公布应用的时候才须要利用这个优化特性。如今的问题是,它究竟能够降低多少文件体积呢?
我最大的一张png图片从2.4MB降低到了2.2MB.小一些的纹理从180kb减至130kb。可能单个文件降低的量并非非常多,但是当你的png图片的总大小有18MB时,它能够使之降低至16MB。
注意,在xcode里面有一项设置,你可能会把它忽略掉。你须要关闭"Compress PNG files"开关,由于这个选项有可能会使你的png图片膨胀。
你能够在xcode的build settings里面设置。例如以下所看到的:
假设激活此png压缩选项,xcode会在png文件打包进程序的时候执行自带的png优化程序。所以。有可能会使我们先前使用TP优化过的png图片再次膨胀。
因此。再次确保这个选项已关闭。
只是即使你没有禁用此选项,你的程序大小还是会有所减小。由于,你有可能使用一些没有被TP优化过的png图片。
检查你的程序在App Store 里面的大小
在Xcode里面,执行Archive build(在菜单中选择Product->Archive)。当build成功的时候,Xcode的Organizer窗体会打开。然后你会看到一个“Estimate Size”(评估大小)的button,能够用来估算你的应用程序大小:
移除未使用的资源文件
在开发游戏的过程中,你会常常加入、移除和替换游戏资源。
所以,你可能会由于某些原因,忘记移除一些不用的图片资源。所以,你须要额外注意把它们都从项目中移除出去,至少要从程序的target中出去。
尤其是你使用多个target的时候(比方,你同一时候维护ipad和mac版本号),你就极有可能会在一个target里面加入一些错误的资源。
当然。在移除资源之后。你一定要充分測试你的游戏。切记!
一定要充分測试。
降低声音文件大小
有时候,我们也会忽视这个问题。
假设你不考虑声音文件的格式,无论是就内存的使用还是程序的大小而言,都是一种极大的浪费。以下是一些方法能够用来降低声音文件的大小。我推荐大家使用一款免费的声音编辑工具。
立体声道变单声道 – 你的mp3文件能够採用立体声。可是,这样做值得吗?假设你听不出来区别的话,建议还是採用单一声道。这样能够把文件大小和内存使用都降低一半。
MP3 比特率 –在iOS设备上面,不论什么比特率大于192kbps的声音都是浪费。
你能够尽量採用低的比特率来获得最好的音质效果,这是一个折中。一般来说,96到128kbps对于mp3文件来说够用了。
採样率 – 大部分的声音文件使用11,22,44,或者48kHz採样率。採样率越低,声音文件越小。可是,这样声音质量也会越低。44kHz已经达到了CD的音质了,而48kHz会更好(这个区别仅仅有调音师才干够听出来)
在大部分情况下。44kHz或者更高的比特率都有点浪费。所以,能够尝试下减小採样率(在Audacity里面:Tarck->Resample)。不要仅仅是改动採样率,由于这样会改变声音文件的音高。
Streaming MP3 Files
mp3文件的播放。首先是载入到内存中,然后解码为未压缩的声音buffer,最后再播放。
就我眼下所知。CocosDenshion的SimpleAudioEngine的playBackgoundMusic是流式播放mp3文件的。流试处理有两个长处:1.更小的内存足迹。
2.解码mp3文件採用ios硬件,而不是cpu。可是,硬件一次仅仅能解码一个文件,假设同一时候播放多个,那么仅仅有一个採用的是硬件解码,其他的都是软件解码。
降低Tilemap大小
很多开发人员没有注意到,tilemap大小太大会消耗大量内存。假设你有一个1000*1000的tilemap,这个大概要消耗1M的内存--假设每个tile消耗一个字节的内存的话。然而,假设每个tile大概消耗64个字节的话。那么这个tilemap就会消耗60MB内存。我的天啊。
除了写一个更好的tilemap外部渲染器,我们能做的唯一一件事就是降低tilemap的大小,该地图可以被划分为两个。