cocos2dx内存优化

纹理消耗了大量内存

在大部分情况下,是纹理(textures)消耗了游戏程序大量的内存。因此,纹理是我们首要考虑优化的对象

纹理加载

cocos2d里面纹理加载分为两个阶段:从图片文件中创建一个Image对象;以这个创建好的Image对象来创建Texture2D对象.加载纹理的文件io操作和纹理创建都是耗时的,需要避免一帧之内加载大量图片资源.因为不仅会导致卡顿还会导致内存过高.最好的方式是多线程加载即异步加载.

使用JPG图片?

cocos2d使用JPG纹理的时候有一个问题,因为JPG纹理在加载的时候,会实时地转化为PNG格式的纹理,而且JPG纹理将消耗三倍于本身内存占用大小的内存。jpg不论在加载速度和内存消耗方面都很差。所以,千万不要大量使用JPG大图.

重视文件图片大小

图片文件大小和纹理内存占用是两码事,图片文件大多是压缩过的,它们被使用的话必须先解压缩,然后才能会GPU所处理,变成我们熟知的纹理。一个2048*2048的png图片,采用32位颜色深度编码,那么它在磁盘上占用空间只有2MB。但是,如果变成纹理,它将消耗16MB的内存!

使用16-bit纹理

最快速地减少纹理内存占用的办法就是把它们作为16位颜色深度的纹理来加载。cocos2d默认的纹理像素格式是32位颜色深度。如果把颜色深度减半,那么内存消耗也就可以减少一半。并且这还会带来渲染效率的提升,大约提高10%。

你可以使用Texture2D对象的类方法setDefaultAlphaPixelFormat来更改默认的纹理像素格式,代码如下:

[Texture2D setDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_RGB5A1];

这里有个问题:首先,纹理像素格式的改变会影响后面加载的所有纹理。因此,如果你想后面加载纹理使用不同的像素格式的话,必须再调用此方法,并且重新设置一遍像素格式。

其次,如果你的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格式比较适合背景图片和一些矩形的用户控件。

RGB5A1格式使用一位颜色来表示alpha通道,因此图片可以拥有透明区域。只是,1位似乎有点不够用,它只能表示32768种可用颜色值。而且图片要么只能全部是透明像素,或者全部是不透明的像素。因为一位的alpha通道的缘故,所以没有中间值。但是你可以使用fade in/out动作来改变纹理的opacity属性。

如果你的图片包含有半透明的区域,那么RBGA4444格式很有用。它允许每一个像素值有127个alpha值,因此透明效率与RGBA8888格式的纹理差别不是很大。但是,由于颜色总量减少至4096,所以,RBGA4444是16位图片格式里面颜色质量最差的。

现在,你可以得到16位纹理的不足之处了:它由于颜色总量的减少,有一些图片显示起来可能会失真

使16位纹理看起来更棒

幸运的是,我们有TexturePacker.(简称TP)

TP有一个特性叫做“抖动”,它可以使得原本由于颜色数量减少而产生的失真问题得到改善。

特别是在拥有Retina显示的像素密度下,你几乎看不出16位与32位的纹理之间的显示差别。当然,前提是你需要采用“抖动”算法。

使用NPOT纹理

NOPT是“non power of two”的缩写,译作“不是2的幂”。NPOT stands for “non power of two”.cocos2dx它默认是支持NPOT的。

如果纹理图集(texture atlas)使用NPOT的纹理,它将有一个具大的优势:它允许TP更好地压缩纹理。因此,我们会更少地浪费纹理图集的空白区域。而且,这样的纹理在加载的时候,会少使用1%到49%左右的内存。而且你可以使用TP强制生成NPOT的纹理。(你只需要勾选“allow free size”即可)

默认使用PVR格式的纹理

TP让你可以创建PVR格式的纹理。除了PVR纹理支持NPOT外,它们不仅可以不是2的幂,而且还可以不是方形的。

PVR是最灵活的纹理文件格式。除了支持标准的未压缩的RGB图片格式外,不支持有损压缩的pvrtc格式。另外,未压缩的pvr格式的纹理的内存消耗非常地低。不像png图片那样要消耗2倍于本身内存占用大小的内存,pvr格式只需要消耗纹理本身内存大小再加上一点点处理该图片格式的内存大小。

pvr格式的一个缺点就是,你不能在Mac上面打开查看。但是,如果你安装了TP的话,就可以使用TP自带的pvr图片浏览器来浏览pvr格式的图片了.

使用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图片格式可以编码更多的颜色值。

预先加载所有的纹理

定要预先加载所有的纹理,你可以在第一个loading场景的时候就全部加载进来。

这样做最大的好处在于,你的游戏体验会表现得非常平滑,而且你不需要再担心资源的加载和卸载问题了。

这样也使得你可以让每一个纹理都使用合适的纹理像素格式,而且可以更方便地找出其它与纹理无关的内存问题。因为如果与纹理有关,那么在第一次加载所有的纹理的时候,这个问题就会暴露出来的。如果所有的纹理都加载完毕,这时候再出现内存问题,那么肯定就与纹理无关了,而是其它的问题了。

按照纹理size从大到小的顺序加载纹理

由于加载纹理时额外的内存消耗问题,所以,采用按纹理size从大到小的方式来加载纹理是一个最佳实践。

假设,你有一个占内存16MB的纹理和四个占用内存4MB的纹理。如果你首先加载4MB的纹理,这个程序将会使用16MB的内存,而当它加载第四张纹理的时候,短时间内会飙到20MB。这时,你要加载16MB的那个纹理了,内存会马上飙到48MB(44 + 162),然后再降到32MB(4*4 + 16)。

但是,反过来,你先加载16MB的纹理,然后短时候内飙到32MB。然后又降到16MB。这时候,你再依次加载剩下的4个4MB的,这时,最多会彪到(43 + 42 + 16=36)MB。

在这两种情况下,内存的峰值使用相差12MB,要知道,可能就是这12MB会断送你的游戏进程的小命哦!

避免在收到内存警告消息的时候清除缓存

纹理已经全部在Loading场景里面加载完毕了,这时候,内存警告发生了,然后cocos2d就会把没有使用的纹理从缓存中释放掉。

你刚刚把所有的纹理都加载进来,还没有进入任何一个场景中(此时所有的纹理都被当作“unused”),但是马上被全部从texture cache中移除出去。可是,你又需要在其它场景中使用它们。在loading场景完了之后进入下一个场景的时候很卡的原因了。cocos2dx 在收到内存警告的时候会自动清理缓存.

理解在什么时候、在哪里去清除缓存

不要随机清除缓存,也可以心想着释放一些内存而去移除没有使用的纹理。那不是好的代码设计。有时候,它甚至会增加加载次数,并多次引发“间歇内存飙高”。分析你的程序的内存使用,看看内存里面到底有什么,以及什么应该被清除,然后只清除该清除的。

你可以使用dumpCachedTextureInfo方法来观察哪些纹理被缓存了:

[[TextureCache] dumpCachedTextureInfo];
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的内存,其它移不移除都没什么太大的影响。

SpriteFrames retain textures

上面提到的例子中,纹理的引用计数可能有点让人看不懂。你会发现,纹理集有很高的retain count,即使你知道这些纹理集中的纹理当前并没有被使用。

你可能忽略了一件事:SprteFrame会retain它的纹理。因此,如果你使用了纹理集,你要完全移除它不是那么容易。因为,由这个纹理集产生的sprite frame还是保留在内存中。所以,你必须调用SpriteFrameCache的removeSpriteFramesFromTexture方法,能彻底清除纹理缓存中的纹理集。

[[CCSpriteFrameCache sharedSpriteFrameCache] removeSpriteFramesFromTexture:uncachedTexture];

你也可以使用 removeSpriteFramesFromFile,并指定一个纹理集的.plist文件来清除缓存起来的精灵帧(spriteframes).

你可以清除任何缓存(比如animation,sprite frames等),但是请不要轻易清除纹理缓存

cocos2d有许多缓存类,比如纹理缓存、精灵帧缓存,动画缓存等。

当然,如果你想从内存中移除一个纹理,你也必须移除与之相关的精灵帧(因为精灵帧会retain纹理)。

例外:检查声音文件的内存使用!

声音文件会被缓存起来,然后可以重复播放而不会被中断。由于声音文件一般比较大,特别是,我看到有一些开发者使用没有压缩的声音文件作为游戏的背景音乐,而这些背景音乐文件非常大,它们通常会造成大量的内存消耗。

请使用MP3格式的声音文件。因为使用没有压缩的声音文件既浪费内存又占用程序大小。当你加载完一些游戏音效时,在不需要的时候,记得要卸载掉。

如何避免缓存特定的纹理

如果你有一个纹理,你确实不想缓存起来,那怎么办呢?比如,在初始的加载场景中的图片,或者那些用户很少会在意的图片--比如你的非常牛比的致谢场景的图片。

经常容易被误解的一点是,一个纹理显示出来了,那么它就被缓存起来了。如果你从缓存中移除此纹理,那么此时你再移除精灵就会程序崩溃。这个理解不正确。

TextureCache只不过是对纹理再添加了一次retain函数的调用,这样,当没有其它对象(比如sprite)持有纹理的引用的时候,纹理仍然会存在内存之间。基于这一点,我们可以立马从缓存中移除出去,这样,当纹理不存需要的时候,马上就会从内存中释放掉。如下代码所示:

        bg = [Sprite spriteWithFile:@"introBG.png"];
        // don't cache this texture:
        [[TextureCache ] removeTextureForKey:@"introBG.png"];

当TextureCache中移除一个纹理的时候,cocos2d下一次在调用spriteWithFile的时候,还是会再加载该纹理的--不管是否有没有一张名字一样的图片正在被其它精灵所使用。因此,如果你不够细心的话,你有可能最后会在内存中加载两张重复的纹理。

使用一个Loading 场景

如果你不能预先加载所有的纹理的话,你可以使用一个loading场景,同时显示一个动画来表明加载的进度。这样可以在进入下一个场景之前,让前面一个场景销毁,同时释放它所占用的内存资源。

实现起来非常简单。这个loading场景调度一个selector,然后每一帧(或者0.1秒也可以)执行一个函数,比如update。除非你前面一个场景有内存泄漏,否则的话,每一次update函数执行的时候,都会把一些引用计数为0的内存资源释放掉。在这个update方法里面,你可以创建新的场景。

这样极大地避免了“间歇性内存飙高”的问题,可以极大地减小内存压力。

在后台加载纹理

TextureCache类还支持异步加载资源的功能,利用addImageAsync方法。你可以很方面地给addImageAsync方法添加一个回调方法,这样,当纹理异步加载结束的时候,可以得到通知。

必须等待一个资源加载完毕。否则的话,由于“间歇性内存飙高”,可能会引发下列问题:

  1. 程序崩溃
  2. 纹理被加载两次!因为异步加载并不能保证加载顺序。

减少你的程序的大小

把纹理的颜色位深度减少到16位,不仅可以减少内存压力,还可以有效地减少程序的体积。但是,我们还有其它方法可以更进一步地减少程序的大小。

TexturePacker PNG 图片优化

如果你有某些原因,让你坚持要使用PNG文件格式而不是我之前极力向你推荐的pvr.ccz文件格式,那么TexturePacker有一个选项,叫做“Png Opt Level”(Png优化级别),可以帮助我们减少png文件的大小

注意,在xcode里面有一项设置,你可能会把它忽略掉。你需要关闭"Compress PNG files"开关,因为这个选项有可能会使你的png图片膨胀。xcode会在png文件打包进程序的时候运行自带的png优化程序。所以,有可能会使我们先前使用TP优化过的png图片再次膨胀。因此,再次确保这个选项已关闭!

不过即使你没有禁用此选项,你的程序大小还是会有所减小。因为,你有可能使用一些没有被TP优化过的png图片。

检查你的程序在App Store 里面的大小

在Xcode里面,运行Archive build(在菜单中选择Product->Archive)。当build成功的时候,Xcode的Organizer窗口会打开,然后你会看到一个“Estimate Size”(评估大小)的按钮,可以用来估算你的应用程序大小:

移除未使用的资源文件

在开发游戏的过程中,你会经常添加、移除和替换游戏资源。所以,你可能会因为某些原因,忘记移除一些不用的图片资源。所以,你需要额外注意把它们都从项目中移除出去,至少要从程序的target中出去。对于android 的so而言可以做一些选择,针对多种cpu架构可以选择一个.

减少声音文件大小

有时候,我们也会忽视这个问题。如果你不考虑声音文件的格式,不管是就内存的使用还是程序的大小而言,都是一种极大的浪费。下面是一些方法可以用来减少声音文件的大小。

立体声道变单声道 – 你的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的大小了,也可以把地图一分为二。

posted on 2018-12-12 10:50  bytemode  阅读(540)  评论(0编辑  收藏  举报

导航