Android SoundPool崩潰問題研究
我們在使用Android 5.x的SoundPool播放短音頻的時候,發現有概率出現SoundPoolThread線程崩潰,現象類似於StackOverflow上的問題:SoundPoolThread causing SIGSEGV via JNI ERROR "accessed deleted global reference"。
根據回答
As you say, this appears to be a bug in Android due to a race condition. As requested by user CommonsWare I opened a bug with the backtrace: http://code.google.com/p/android/issues/detail?id=53043
看起來應該是Android特定版本的bug。
可惜即使翻了牆,我也看不到這個issue的內容。
我們看了下安卓的源碼,發現在Android 5.x和Android 6+時,Android SoundPool的代碼結構也發生了變化,從原先分散的
frameworks/av/include/media/SoundPool.hframeworks/av/media/libmedia/SoundPool.cppframeworks/av/media/libmedia/SoundPoolThread.cppframeworks/av/media/libmedia/SoundPoolThread.hframeworks/base/media/jni/soundpool/android_media_SoundPool_SoundPoolImpl.cppframeworks/base/media/java/android/media/SoundPool.javaframeworks/base/media/test/SoundPoolTest/src/com/android/SoundPoolTest.java
變成了:
frameworks/base/media/jni/soundpool/SoundPool.cppframeworks/base/media/jni/soundpool/SoundPool.hframeworks/base/media/jni/soundpool/SoundPoolThread.cppframeworks/base/media/jni/soundpool/SoundPoolThread.hframeworks/base/media/jni/soundpool/android_media_SoundPool.cppframeworks/base/media/java/android/media/SoundPool.javaframeworks/base/media/tests/SoundPoolTest/src/com/android/SoundPoolTest.java
可以看出從原先Android 5.x的frameworks/av+frameworks/base統一到了Android 6+的frameworks/base。
通過進一步分析代碼,發現https://android.googlesource.com/platform/frameworks/base/+/ed86e19af2e36397a1cd5b89105b1bf0de47414e修改,根據修改說明
Race-condition in SoundPool during release
There is race between SoundPoolThread and SoundPool / AudioManagerthreads during releasing SoundPool.AudioManager deletes a global reference before setting SoundPoolcallback to NULL. If, at that time, a call to the SoundPool::notify
fuction happens then mCallback is valid but mUserData is not.The following log will show up to indicate the problem: JNI ERROR (app bug): accessed deleted global reference 0xXXXXXXXXThis fix is to clear the SoundPools callback before releasing globalreference.
Change-Id: I5e6d647edc0444340db879428048e2c0a068a8b4
其修改內容是:
SoundPool *ap = MusterSoundPool(env, thiz); if (ap != NULL) { - // release weak reference+ // release weak reference and clear callback jobject weakRef = (jobject) ap->getUserData();+ ap->setCallback(NULL, NULL); if (weakRef != NULL) { env->DeleteGlobalRef(weakRef); } - // clear callback and native context- ap->setCallback(NULL, NULL);+ // clear native context env->SetLongField(thiz, fields.mNativeContext, 0); delete ap; }
即調整了setCallback和DeleteGlobalRef的順序。
根據我們出問題的現象來推斷,如果先執行了DeleteGlobalRef,有可能會導致SoundPool(java)被垃圾回收,而此時如果C++調用了SoundPool::notify()來訪問SoundPool(java)的話,進程就會崩潰,跟提交日誌的說法基本是對的上的。
由於Android 6.0的代碼已經修復了此問題,所以我們確認這個問題應該是在Android < 6.0上存在並被Android 6.0所修復的問題。
那麼,這個問題是如何出現的呢?
以下是我們有問題的實現:
public static void playSound(String assetPath) { SoundPool.Builder builder = new SoundPool.Builder(); builder.setMaxStreams(1); AudioAttributes attr = new AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_MUSIC).build(); builder.setAudioAttributes(attr); SoundPool soundPool = builder.build(); try { AssetFileDescriptor assetFileDescriptor = Application.ASSET.openFd(assetPath); final int id = soundPool.load(assetFileDescriptor,0); soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() { @Override public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { soundPool.play(id,1,1,1,0,1); } }); } catch (IOException e) { e.printStackTrace(); } }
這個代碼有幾個問題:
1 是把SoundPool作為一個臨時變數來使用,那麼SoundPool啥時被垃圾回收呢?不一定是聲音播完,理論上聲音還沒播放都有可能被垃圾回收。
2 先調用load再調用setOnLoadCompleteListener,如果在load後Sleep了幾秒的話,onLoadComplete回調就可能永遠也不調用了。
所以,參考cocos2d-x中對SoundPool的使用,我們改為了:
public static void playSound(String assetPath){ if (mSoundPool == null) { SoundPool.Builder builder = new SoundPool.Builder(); builder.setMaxStreams(8); AudioAttributes attr = new AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_MUSIC).build(); builder.setAudioAttributes(attr); mSoundPool = builder.build(); mSoundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() { @Override public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { soundPool.play(sampleId,1,1,1,0,1); } }); } try { AssetFileDescriptor assetFileDescriptor = Application.ASSET.openFd(assetPath); mSoundPool.load(assetFileDescriptor,0); } catch (IOException e) { e.printStackTrace(); } }
要點:
- 保證SoundPool的生存期長於要播放的音頻,所以經常用單例來存儲SoundPool。
- 先設置setOnLoadCompleteListener回調。並且設置一次就好。
- setOnLoadCompleteListener的回調中參數sampleId跟load的返回值都是sampleId,使用回調中的參數可以避免原先代碼中需要先調用load後調用setOnLoadCompleteListener的順序問題。
- 如果需要允許同時混多路音頻,需要調整builder.setMaxStreams(),如果設置為1,則播放第2個音頻時,就會覆蓋第1個音頻。
推薦閱讀:
※錄音時只有一個麥克風,音樂應該大多是單聲道的,那為什麼我們下載的音樂普遍是雙聲道呢?
※有哪些好的音效製作、音效剪輯、聲音設計、混音和音樂錄製類軟體?
※錄音入門求推薦一些設備?
※使用單 Room麥方式有啥好處?鋁帶的?
※貼人聲必須要用壓縮嗎?