天天看點

【Himi轉載推薦之一】優化cocos2d/x程式的記憶體使用和程式大小【一】

貌似從Himi建博以來,從來沒出現轉載過博文的說。但是從今天起,Himi将一些非常好、很值得推薦的博文進行轉載分享!大家如果有好的文章,請将博文連結或者内容郵件給我,[email protected]    [email protected]

首先感謝 子龍 兄弟對文章翻譯的貢獻,子龍部落格原文位址:

<a href="http://www.cnblogs.com/zilongshanren/archive/2012/12/09/2810017.html">http://www.cnblogs.com/zilongshanren/archive/2012/12/09/2810017.html</a>

再次感謝原創者:Steffen Itterheim,原創部落格原文位址:

<a href="http://www.learn-cocos2d.com/2012/11/optimize-memory-usage-bundle-size-cocos2d-app/">http://www.learn-cocos2d.com/2012/11/optimize-memory-usage-bundle-size-cocos2d-app/</a>

譯者:

因為有着c/c++背景的我,看到這句話的時候,很是贊同,是以剛開始cocos2d程式設計的時候格外注意記憶體方面的問題。即便如此,在我完成自己第一個遊戲的過程中,還是遇到了大量的記憶體問題,它們讓我頭疼,讓我睡不着覺。慶幸的是,我通過社群都找到了答案并且解決了我的問題。

全文如下:

我目前正完成我的最後一個合約項目。在這個項目的最後階段,我需要考慮的一件事情就是如何優化遊戲的記憶體使用。

我還會給大家介紹,如何在你加載遊戲資源的時候展示一個帶有動畫的Loading界面,我還會加入一些最佳實踐和小技巧。

大家猜一下:)

在大部分情況下,是紋理(textures)消耗了遊戲程式大量的記憶體。是以,紋理是我們首要考慮優化的對象,特别是當你碰到記憶體警告的問題的時候。

cocos2d裡面紋理加載分為兩個階段:1.從圖檔檔案中建立一個UIImage對象。2.以這個建立好的UIImage對象來建立CCTexture2D對象。這意味着,當一個紋理被加載的時候,在短時候内,它會消耗兩倍于它本身記憶體占用的記憶體大小。(譯注:為什麼隻是短時間内呢?因為autoRelease pool和引用計數的關系,臨時建立的UIImage對象會被回收。)

當你在一個方法體内,接二連三地加載4個紋理的時候,這個記憶體問題會變得更加糟糕。因為在這個方法還沒結束之前,每一個紋理都會消耗兩倍于它本身的記憶體。

我不是很确定,現在的cocos2d是否仍然如此。或者這種情況是否隻适用于手工引用計數管理,或許ARC不會如此呢?我習慣于按順序加載紋理,但是在加載下一個紋理之前要等待一幀。這将會使得任何紋理加載的消耗對記憶體的壓力降低。因為等待一幀,引用計數會把臨時的UIImage對象釋放掉,減少記憶體壓力。此外,在後續的文章中,如果你想在背景線程中按序加載紋理的話,也可以采用這種方法。

一個2048*2048大小的紋理會消耗16M的記憶體。當你加載它的時候,在短時間内,它将消耗32MB記憶體。現在,如果這個圖檔是JPG格式,你會看到這個數字會達到48MB,因為額外的UIImage對象的建立。雖然,最終記憶體都會降到16M,但是,那一個時刻的記憶體飙高,足以讓os殺死你的遊戲程序,造成crash,影響使用者體驗。

JPG不論在加載速度和記憶體消耗方面都很差。是以,千萬不要使用JPG!

這種情況,我見到很多。它乍聽起來可能覺得有點荒誕,但事實如此,因為它需要關于檔案格式的知識,而這些知識并不是每一個程式員都了解的。我經常聽到的論斷就是“嘿!我的程式不可能有記憶體警告,我所有的圖檔資源加起來還不到30MB!”。

怎麼說呢,因為圖檔檔案大小和紋理記憶體占用是兩碼事。假設他們是帳篷。圖檔檔案就相當于帳篷被裝在行李箱。但是,如果你想要使用帳篷的話,它必須被撐起來,被“膨脹”。

圖檔檔案和紋理的關系與此類似。圖檔檔案大多是壓縮過的,它們被使用的話必須先解壓縮,然後才能會GPU所處理,變成我們熟知的紋理。一個2048*2048的png圖檔,采用32位顔色深度編碼,那麼它在磁盤上占用空間隻有2MB。但是,如果變成紋理,它将消耗16MB的記憶體!

當然,減少紋理占用記憶體大小是有辦法滴。

最快速地減少紋理記憶體占用的辦法就是把它們作為16位顔色深度的紋理來加載。cocos2d預設的紋理像素格式是32位顔色深度。如果把顔色深度減半,那麼記憶體消耗也就可以減少一半。并且這還會帶來渲染效率的提升,大約提高10%。

這裡有個問題:首先,紋理像素格式的改變會影響後面加載的所有紋理。是以,如果你想後面加載紋理使用不同的像素格式的話,必須再調用此方法,并且重新設定一遍像素格式。

其次,如果你的CCTexture2D設定的像素格式與圖檔本身的像素格式不比對的話,就會導緻顯示嚴重失真。比如顔色不對,或者透明度不對等等。

RGBA8888是預設的格式。對于16位的紋理來說,使用RGB565可以獲得最佳顔色品質,因為16位全部用來顯示顔色:總共有65536總顔色值。但是,這裡有個缺點,除非圖檔是矩形的,并且沒有透明像素。是以RBG565格式比較适合背景圖檔和一些矩形的使用者控件。

RBG5A1格式使用一位顔色來表示alpha通道,是以圖檔可以擁有透明區域。隻是,1位似乎有點不夠用,它隻能表示32768種可用顔色值。而且圖檔要麼隻能全部是透明像素,或者全部是不透明的像素。因為一位的alpha通道的緣故,是以沒有中間值。但是你可以使用fade in/out動作來改變紋理的opacity屬性。

如果你的圖檔包含有半透明的區域,那麼RBGA4444格式很有用。它允許每一個像素值有127個alpha值,是以透明效率與RGBA8888格式的紋理差别不是很大。但是,由于顔色總量減少至4096,是以,RBGA4444是16位圖檔格式裡面顔色品質最差的。

現在,你可以得到16位紋理的不足之處了:它由于顔色總量的減少,有一些圖檔顯示起來可能會失真,而且可能會産生“梯度”。

特别是在擁有Retina顯示的像素密度下,你幾乎看不出16位與32位的紋理之間的顯示差别。當然,前提是你需要采用“抖動”算法。

cocos2d預設的顔色深度将會把所有的紋理都渲染到16位的color framebuffer裡面,然後再顯示到你的裝置螢幕上面。既然這樣,我們為什麼不把所有的紋理的格式都弄成16位呢,32位又有什麼用呢?反正它本來就會渲染到16位的framebuffer上去的。這個問題有點太底層了,我不想深挖下去,而且我也不适合解釋這個問題。(譯者:哈哈,知之為知之,不知為不知)

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”即可)

TP讓你可以建立PVR格式的紋理。除了PVR紋理支援NPOT外,它們不僅可以不是2的幂,而且還可以不是方形的。

PVR是最靈活的紋理檔案格式。除了支援标準的未壓縮的RGB圖檔格式外,不支援有損壓縮的pvrtc格式。另外,未壓縮的pvr格式的紋理的記憶體消耗非常地低。不像png圖檔那樣要消耗2倍于本身記憶體占用大小的記憶體,pvr格式隻需要消耗紋理本身記憶體大小再加上一點點處理該圖檔格式的記憶體大小。

pvr格式的一個缺點就是,你不能在Mac上面打開檢視。但是,如果你安裝了TP的話,就可以使用TP自帶的pvr圖檔浏覽器來浏覽pvr格式的圖檔了。(強烈建議大家購買TP,支援TP,不要再盜版了)

使用PVR格式的檔案幾乎沒有缺點。此外,它還可以極大地提高加載速度,後面我會解釋到。

當在cocos2d裡面使用pvr格式的紋理時,隻使用pvr.ccz格式,不要使用其它格式!因為它加載速度超快,而且加載的時候使用更少的記憶體!

這裡把32位的png圖檔(左邊)與最佳品質的PVRTC4(4位)圖檔(點選圖檔檢視完整的大小)作對比:

注意,在一些高對比度的地方,明顯有一些瑕疵。有顔色梯度的地方看起來還好一點。

PVRTC肯定不是大部分遊戲想要采用的紋理格式。但是,它們對于粒子效果來說,非常适用。因為那些小的粒子在不停地移動、旋轉、縮放,是以你很難看出一些視覺瑕疵。

TP提供的PVR格式不僅有上面兩種,還包括TC2和TC4這兩種沒有alpha通道的格式。

這裡的alpha和16位紋理的alpha是一樣的。沒有alpha通道意味着圖檔裡面沒有透明像素,但是,更多的顔色位會用來表示顔色,那麼顔色品質看起來也會更好一些。

有時候,PVRTC圖檔格式指的是使用4位或者2位顔色值 ,但是,并不完全是那樣。PVRTC圖檔格式可以編碼更多的顔色值。

就像标題所說,盡你所能,一定要預先加載所有的紋理。如果你的所有的紋理加起來不超過80MB記憶體消耗的話(指的是擁有Retina顯示的裝置,非Retina的減半考慮),你可以在第一個loading場景的時候就全部加載進來。

這樣做最大的好處在于,你的遊戲體驗會表現得非常平滑,而且你不需要再擔心資源的加載和解除安裝問題了。

這樣也使得你可以讓每一個紋理都使用合适的紋理像素格式,而且可以更友善地找出其它與紋理無關的記憶體問題。因為如果與紋理有關,那麼在第一次加載所有的紋理的時候,這個問題就會暴露出來的。如果所有的紋理都加載完畢,這時候再出現記憶體問題,那麼肯定就與紋理無關了,而是其它的問題了。

如果你知道問題與紋理無關的話,那麼你查找剩下的記憶體問題将會變得更加簡單。而且你避免了前面說的這種情況:當2048*2048的紋理加載的時候,它本來隻需要消耗16MB記憶體,但是短時間會沖到32MB記憶體。後面會提出一種方法來解決“間歇性記憶體飙高”(“譯者發明滴”)的方法。(譯者:希望下次開發者的對話中“間歇性記憶體飙高”的說法會出現,呵呵)

由于加載紋理時額外的記憶體消耗問題,是以,采用按紋理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中,如果沒有就加載。你會在後面看到具體的做法。

不要随機清除緩存,也可以心想着釋放一些記憶體而去移除沒有使用的紋理。那不是好的代碼設計。有時候,它甚至會增加加載次數,并多次引發“間歇記憶體飙高”。分析你的程式的記憶體使用,看看記憶體裡面到底有什麼,以及什麼應該被清除,然後隻清除該清除的。

這個方法的輸出如下:(為了清楚起見,我把那些與-hd字尾有關的資訊屏蔽掉了) 

 上面包含了非常多有用的資訊。紋理的大小、顔色深度(bpp)和每一個被緩存的紋理在記憶體中所占用大小等。這裡的“rc”代表紋理的“引用計數”。如果這個引用計數等于1或2的話,那麼意味着,這個紋理目前可能不會需要使用了,此時,你可以放心地把它從紋理cache中移除出去。

你隻移除你知道在目前場景下不太可能會被使用的紋理(即上面介紹的引用計數為1或2的情況),這是一個明智的做法。另外,隻移除那些占用記憶體大的紋理。如果一個紋理隻占幾個kb的記憶體,其它移不移除都沒什麼太大的影響。(譯注:這就和程式優化一樣,不要做過多的細節優化,不要過早優化,要找到性能的瓶頸,然後再重點優化,以20%的時間換取80%的效率。過早和過多細節優化對于大多數程式而言,是需要極力避免的)。

上面提到的例子中,紋理的引用計數可能有點讓人看不懂。你會發現,紋理集有很高的retain count,即使你知道這些紋理集中的紋理目前并沒有被使用。

Note: 這一點隻針對cocos2d v1.0有效,而cocos2d v2.x在加載之前會預先判斷。

這樣看起來有點無知(innocent):

但是,要注意,CCSpriteFrameCache并不會去檢查一個精靈幀是否已經被緩存起來了!這與CCTextureCache的動作方式有所不同,它每次都會去加載spriteframes.

這個過程到底需要耗費多少時間呢,這取決于你提供的.plist檔案中精靈幀的數量。我注意到,隻有14幀的plist加載與有280幀的plist加載有着很大的差別。是以,對于精靈幀的加載,你也需要謹慎。

是以,你要避免一些不必要的addSpriteFrames*方法調用。因為那邊導緻場景切換時産生小的卡頓。

cocos2d有許多緩存類,比如紋理緩存、精靈幀緩存,動畫緩存等。但是,如果你想清理記憶體的話,精靈幀緩存和動畫緩存對記憶體的占有是非常少的,可以說是極少的。

當然,如果你想從記憶體中移除一個紋理,你也必須移除與之相關的精靈幀(因為精靈幀會retain紋理)。說白了,不要輕易去移除精靈幀和動畫緩存,因為你有可能會使用到一個沒有緩存的動畫幀對象或者精靈幀對象,那樣會導緻程式crash。

聲音檔案會被緩存起來,然後可以重複播放而不會被中斷。由于聲音檔案一般比較大,特别是,我看到有一些開發者使用沒有壓縮的聲音檔案作為遊戲的背景音樂,而這些背景音樂檔案非常大,它們通常會造成大量的記憶體消耗。

請使用MP3格式的聲音檔案。因為使用沒有壓縮的聲音檔案既浪費記憶體又占用程式大小。當你加載完一些遊戲音效時,在不需要的時候,記得要解除安裝掉。在第二篇文章中,我會向大家介紹有于聲音檔案更多的知識。

如果你有一個紋理,你确實不想緩存起來,那怎麼辦呢?比如,在初始的加載場景中的圖檔,或者那些使用者很少會在意的圖檔--比如你的非常牛比的緻謝場景的圖檔。

經常容易被誤解的一點是,一個紋理顯示出來了,那麼它就被緩存起來了。如果你從緩存中移除此紋理,那麼此時你再移除精靈就會程式崩潰。這個了解不正确。

CCTextureCache隻不過是對紋理再添加了一次retain函數的調用,這樣,當沒有其它對象(比如sprite)持有紋理的引用的時候,紋理仍然會存在記憶體之間。基于這一點,我們可以立馬從緩存中移除出去,這樣,當紋理不存需要的時候,馬上就會從記憶體中釋放掉。如下代碼所示:

 你需要記住,當你從CCTextureCache中移除一個紋理的時候,cocos2d下一次在調用spriteWithFile的時候,還是會再加載該紋理的--不管是否有沒有一張名字一樣的圖檔正在被其它精靈所使用。是以,如果你不夠細心的話,你有可能最後會在記憶體中加載兩張重複的紋理。

有一個例子就是,當你在循環中加載紋理,而這些紋理你并不想緩存起來。這種情況下,你就需要在循環之外去移除此紋理的緩存,否則可能會導緻多個紋理被重複加載到記憶體之中:

 上面這個例子是我從highscore場景中摳出來的,一旦此場景退出,就不應該持有CCLabelAtlas紋理的引用。是以,我們需要把它從紋理緩存中移除出去。但是,你必須防止重複加載紋理到記憶體中去。

通過這種方式,我們可以非常友善地清除緩存中的紋理,而且最好是在建立紋理的時候清除,而不要在其它地方,比如dealloc或者索性讓purge cache去做這個事。

如果你不能預先加載所有的紋理的話,你可以使用一個loading場景,同時顯示一個動畫來表明加載的進度。這樣可以在進入下一個場景之前,讓前面一個場景銷毀,同時釋放它所占用的記憶體資源。

 實作起來非常簡單。這個loading場景排程一個selector,然後每一幀(或者0.1秒也可以)執行一個函數,比如update。除非你前面一個場景有記憶體洩漏,否則的話,每一次update函數執行的時候,都會把一些引用計數為0的記憶體資源釋放掉。在這個update方法裡面,你可以建立新的場景。

這樣極大地避免了“間歇性記憶體飙高”的問題,可以極大地減小記憶體壓力。

這一點非常重要:你必須等待一個資源加載完畢。否則的話,由于“間歇性記憶體飙高”,可能會引發下列問題:

1) 程式崩潰

2) 紋理被加載兩次!因為異步加載并不能保證加載順序。

 裡面的selector方法隻接收一個object參數(但是并沒有使用)。然後就可以在此這方法裡面異步加載資源了,如下所示:

 這樣做最大的好處在于,你加載資源的同時,loading場景還可以播放動畫,可以添加精靈并運作一些action,這一切可以處理得很平滑。這種優勢甚至在單個CPU的機器上面也表現得不錯,但是如果你的裝置有多個cpu的話效果更佳。

但是,你需要注意,你不能在背景線程加載紋理,你必須使用addImageAsync方法。這是因為紋理必須與公共的OpenGL context在相同的線程中加載。這樣,你就必須先異步加載紋理,然後再去背景加載sprite frames.你不能依靠CCSpriteFrameCache在背景線程中加載紋理。

下面的代碼,是我采用的異步加載紋理和精靈幀的方法(在另外一個線程中加載:)

假設loadAssetsThenGotoMainMenu方法每一幀都會被觸發。assetLoadCount和loadingAsset變量被聲明在類接口中,分别 是init和bool類型:

 當這個方法運作到第一個case語句的時候,為了避免同樣的圖檔被加載多次,我們把loadingAsset标記設定為yes。當紋理加載完後,我們就添加increaseAssetLoadCount(這個數量可以用來顯示進度條加載百分比)。後面的case語句還可以加載更多的其它東西,比如聲音、字型檔案、粒子效果、實體配置檔案、關卡資訊等。不管加載多少東西,最後的default語句會執行,然後就可以進入MainMenuScene了。

這個方法的通用之處是,你可以通過case與assetLoadCount來異步加載多個紋理,同時又能避免“間歇性記憶體飙高”的問題。因為每幀調用一次方法的時候,前面紋理加載多出來的臨時記憶體已經被釋放掉了。因為目前線程棧頂的autoRelease pool會在每一幀渲染之前被清空。

後記:這裡介紹的内容雖然是針對cocos2d-iphone的,但是,絕大部分内容是适合cocos2d-x的。是以,開發者大可放心去試用這些方法,如果大家有更好的優化遊戲記憶體使用的方法,歡迎分享。希望此帖能成為cocos2d記憶體問題的終極解決方案帖。如果大家覺得我翻譯的不錯,希望能點一下旁邊的推薦按鈕。Thanks, enjoy!:)

Happy coding!

如何減少遊戲程式大小?(以前的目标是20MB以下,現在的目标是50MB以下,為什麼?你懂的!)

   且聽下回分解:)

本文轉自 xiaominghimi 51CTO部落格,原文連結:http://blog.51cto.com/xiaominghimi/1085955,如需轉載請自行聯系原作者

繼續閱讀