標籤:

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: code.google.com/p/andro

看起來應該是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。

通過進一步分析代碼,發現android.googlesource.com修改,根據修改說明

Race-condition in SoundPool during release

There is race between SoundPoolThread and SoundPool / AudioManager

threads during releasing SoundPool.

AudioManager deletes a global reference before setting SoundPool

callback 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 0xXXXXXXXX

This fix is to clear the SoundPools callback before releasing global

reference.

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(); } }

要點:

  1. 保證SoundPool的生存期長於要播放的音頻,所以經常用單例來存儲SoundPool。
  2. 先設置setOnLoadCompleteListener回調。並且設置一次就好。
  3. setOnLoadCompleteListener的回調中參數sampleId跟load的返回值都是sampleId,使用回調中的參數可以避免原先代碼中需要先調用load後調用setOnLoadCompleteListener的順序問題。
  4. 如果需要允許同時混多路音頻,需要調整builder.setMaxStreams(),如果設置為1,則播放第2個音頻時,就會覆蓋第1個音頻。

推薦閱讀:

錄音時只有一個麥克風,音樂應該大多是單聲道的,那為什麼我們下載的音樂普遍是雙聲道呢?
有哪些好的音效製作、音效剪輯、聲音設計、混音和音樂錄製類軟體?
錄音入門求推薦一些設備?
使用單 Room麥方式有啥好處?鋁帶的?
貼人聲必須要用壓縮嗎?

TAG:Android | 混音 |