CoCos2D-X 2.2遊戲卡死調試方案求解?
遊戲在前幾個星期版本更新後, 戰鬥會出現一定幾率卡死的情況, 尚不知重現方法, 重現幾率較低, 但可通過大量嘗試進行重現, 故重現成本較高, 卡死表現為整個進程卡死, 畫面停滯, 輸入無反應, 過一段時間, 提示無響應, 但背景音樂正常播放, 初步推斷是某個地方死循環或IO阻塞導致. 在PC端無法重現, 在android平台出現頻率較高, ios平台也有報告過, 但只有幾例.
遊戲引擎採用quick cocos2d-x開發, 基於cocos2d-x 2.2版本, 集成CoCoStudio作為UI與動畫支持, 邏輯層採用lua開發.前期解決思路:1) 檢查卡死版本所做的戰鬥方面的改動, 在關鍵地方加log, 未發現有可疑的地方.2) 給lua層所有while, for, repeat加log, 在每次循環前記錄一個時間戳, 然後在循環的第一行調用一個函數進行檢測, 超過一定時間, 就列印堆棧. 經重現, 未見有log列印.3) 給lua加hook, 但由於加hook後, 遊戲運行效率太低, 無法在手機上順暢運行, 從而將加大重現的難度, 故放棄這一方案.4) 在重現出卡死時, 用"debuggerd -b 進程ID"的方法列印出遊戲相關的堆棧, 列印了幾次, 都只看到CallStaticIntMethod, playEffect, CocosDenshion等相關調用堆棧(具體記錄文件不在手頭, 只能憑記憶寫出大概), 懷疑卡死是否與之有關, 但音樂,音效部分應是獨立線程.5) 嘗試用gdb調試, 在build_native里加NDK_DEBUG開關, 編譯帶符號的libgame.so, 打算利用ndk-gdb進行遠程調試, 但正常遊戲運行, 連接上後, 用bt列印當前調用堆棧, 只能列印出libc.so的相關堆棧, 無法列印到libgame.so層, 如果直接給libgame.so層的函數加斷點, 可以觸發斷點並列印出斷點處的堆棧及具體代碼信息, 故排除solib-search-path, directory等的設置問題. 綜上, 有幾個問題可能成為解決問題的關鍵, 望不吝賜教:1) 是否有更好的方案或思路解決該問題?2) 是否有更高效的調試工具可以幫助解決該問題?3) gdb 直接bt為何無法列印出libgame.so的當前運行堆棧? 但加函數斷點在CCDirector的mainloop上卻能觸發, 並列印出堆棧.4) 是否有高效的lua調試方法或遠程調試器能幫助了解lua在卡死前或卡死當前的調用情況? 5) CocosDeshion 的相關調用有點可疑, 因為遊戲正常運行情況下, 用debuggerd列印堆棧, 鮮少見其相關調用, 卡死時, 多次調用都只有CocosDeshion的相關調用, 其是否有可能與卡死有關.在下是手游開發方面的新手, 對Android, linux, Cocos2d-x相關底層機理了解不深, 有敘述或理解上的不當, 也請不吝賜教.
在這裡貼上修復這個問題的PR:
SimpleAudioEngine:Fixed thread safety problem on android. by WenhaiLin · Pull Request #12256 · cocos2d/cocos2d-x · GitHub
關注到有點晚了,抱歉問題在這個星期查出來了, 應該算是cocos2D-x引擎在android平台實現上的一個缺陷吧, 特此記錄一下, 希望對後來人有幫助吧.
首先說一下關於gdb的bt指令無法列印遊戲引擎相關運行堆棧的問題, 似乎是與attach的id有關. 之前attach的是ps出來的遊戲進程id, 然後進入gdb, info threads, 似乎看不到libgame.so相關的線程, 後來用"debugd -b 遊戲進程id", 可以看到libgame.so相關的線程, 然後直接attach該線程對應的sysTid, 進入gdb後, 似乎就可以找到libgame.so相關的線程了, 原因尚未深究.
接下來就是正題了, 這個卡死的問題, 是由於Cocos2dxSound.java設計上存在線程不安全缺陷引起的. 卡死時main線程或gl線程會拋出ConcurrentModificationException. 如果是gl線程拋出這個異常, 就會直接導致畫面和整個遊戲完全卡死.
先讓我們看看Cocos2dxSound.java的這段代碼,
首先, 這介面是用來播放音效的, 由glThread線程(也就是我們遊戲的主線程)調用, 它會先去mPathSoundIDMap試圖拿對應音效的soundId, 如果有則直接調用doPlayEffect介面播放音效並生成一個streamID, 如果沒有, 則調用preloadEffect預載入音效, 然後調用this.mSemaphore.acquire();
mSemaphore對象維護一個"許可數量", acquire方法每調用一次會消耗掉一個"許可", 當許可為0時, 就會阻塞等待, 直到有新的"許可"可以領取為止. mSemaphore的許可數量默認初始化為0.
注意到 this.mSemaphore.acquire(); 上面的這句注釋 "wait OnloadedCompleteListener to set streamID", 意思是阻塞等待音效載入完畢後, main線程(注意: 不是gl Thread)會調用OnloadedCompleteListener 回調, 這個回調會調用doPlayEffect介面播放音效並生成一個streamID, 然後發放一個許可, 之後 this.mSemaphore.acquire()會得到一個許可, 解除線程阻塞, 並且設置streamID返回.
可遺憾的是, 這個設計是有缺陷的. 做一個簡單的假設,
你單獨調用了一次preloadEffect介面載入音效A, 這個時候音效A載入完畢後 OnloadedCompleteListener 依然會被調用, 然後mSemaphore它會發放一個"許可", 但沒有人去mSemaphore.acquire()消耗掉這個許可.
接著你調用playEffect介面去播放音效B, 這個時候音效B尚未被載入, 故依然會調用preloadEffect去載入音效B, 然後調用mSemaphore.acquire() 打算阻塞等待音效載入完畢, 遺憾的是, 由於之前單獨預載入音效A時已經發放過一個"許可"了, 所以這裡的acquire不用阻塞等待了, 它直接拿了這個許可就繼續執行下去了, 這樣gl Thread線程不會被阻塞, 而且會返回一個錯誤的streamID, 而main線程會在音效B載入完畢後照常調用OnloadedCompleteListener , 並播放音效B. 只是gl Thread線程不被阻塞, 它可以在回調期間, 繼續調用doPlayEffect, stopEffect, preloadEffect, unloadEffect等介面, 這些介面都有可能會對mPathSoundIDMap這個線程不安全的ArrayList及其同樣線程不安全的子ArrayList進行操作.
然後如果兩個線程(glThread和main)剛好同時對mPathSoundIDMap或其子ArrayList進行了操作, 就會導致ArrayList修改衝突, 某個線程拋出ConcurrentModificationException異常, 悲劇的事情就這樣發生了.
異常有可能從main線程拋出, 也有可能從gl thread線程拋出, 如果直接從gl thread線程拋出, 那麼遊戲直接卡死. 如果從main線程拋出, 後續gl thread的相關acquire會被不斷消耗, 但OnloadedCompleteListener 卻不會再調用, 也就是許可不會再增加, 如果很不幸acquire消耗殆盡了, 它會阻塞gl thread. 這也就能解釋了我上面問題中說的, 卡死時很多次debuggerd打出來的堆棧都是這段:
(_JNIEnv::CallStaticIntMethod(_jclass*, _jmethodID*, ...)+18)(playEffectJNI+52)(CocosDenshion::SimpleAudioEngine::playEffect(char const*, bool)+14)因為這個時候調用playEffect阻塞在acquire這裡了.
基於這種設計缺陷, 如果你的遊戲為了流暢性, 單獨調用了cocos2d-x引擎暴露給我們的preloadEffect介面, 對音效進行了預載入. 而且你沒能把所有將會播放的音效都進行preloadEffect預載入或是預載入後中途又unload了, 那麼當調用playEffect播放一個還沒載入的音效時, 首先會得到一個錯誤的streamID, 這會導致之後你對這個音效的相關操作失效, 諸如無法暫停或無法停止該音效. 其次, 如果這期間你對其他音效進行一些stopSound, unloadEffect等操作, 有一定概率會導致遊戲卡死.
解決方法:
1) 不預載入音效
2) 嚴格預載入所有關卡將會播放的音效, 並且在關卡進行期間不unload它們, 等到關卡結束後再統一清理.3) 改Cocos2dxSound.java的實現。。。。。。上github看了下, 似乎Cocos2dxSound.java的實現至今仍是如此. 各位留心.您好,小弟新手項目中遇到了和您同樣的問題,遊戲卡死畫面停滯, 但有時背景音樂還在正常播放,能否告知您的QQ,好仔細向您請教一番
您 好 我也遇見過這個問題 遊戲卡死
推薦閱讀:
※iOS 如何判斷用戶是購買正版的用戶還是越獄用戶?
※如何升級iOS 9?
※iphone圖庫打開為什麼縮略圖不需要緩衝載入時間?
※為什麼第三種寫法不對呢?