Android語音訊息播放(MediaPlayer) 踩坑
引言
最近專案中的IM模組收到反映,語音訊息點了之後正在播放卻沒有聲音,有時甚至直接ANR異常,因專案中的IM採用的是網易的雲信,所以第一時間請教了雲信的技術人員,得到的回覆是他們的SDK播放語音是直接封裝呼叫了系統的Api,沒有做任何處理。既然這樣,那就只好自己研究下問題啦
問題定位
首先從IM的SDK中的語音播放類入手,發現確實是呼叫了Android的系統語音播放。
IM的SDK原始碼
那麼我們去看一下Android的媒體播放類MediaPlayer的這幾個方法的原始碼,分析一下問題,先看一下MediaPlayer的setDataSource
方法,
setDataSource
通過註釋可以看到,這個方法是支援傳遞本地檔案路徑或者是一個網路路徑的,猜測是否是因為在ui執行緒載入網路資源,導致了anr,我們接著往下看
setDataSource
的過載方法裡對傳入的資料來源做了區分,最後呼叫了native的setDataSource方法。
然後我們看一下prepare
方法
從註釋可以看到,prepare
方法還有另外一個prepareAsync
方法,
根據註釋可以看到,prepareAsync
方法是非同步的去準備資源,基本驗證了我們之前的猜想,因為他們最終都是呼叫了c++層的程式碼,這裡我們直接去看一下他們的原始碼
原始碼位置frameworks/av/media/libmedia/mediaplayer.cpp
status_t MediaPlayer::prepare() { ALOGV("prepare"); Mutex::Autolock _l(mLock); mLockThreadId = getThreadId(); if (mPrepareSync) { mLockThreadId = 0; return -EALREADY; } mPrepareSync = true; status_t ret = prepareAsync_l(); if (ret != NO_ERROR) { mLockThreadId = 0; return ret; } if (mPrepareSync) { mSignal.wait(mLock); // wait for prepare done mPrepareSync = false; } ALOGV("prepare complete - status=%d", mPrepareStatus); mLockThreadId = 0; return mPrepareStatus; } status_t MediaPlayer::prepareAsync() { ALOGV("prepareAsync"); Mutex::Autolock _l(mLock); return prepareAsync_l(); }
可以看到,不管是prepare
還是prepareAsync
方法,最終都是會呼叫prepareAsync_l()
,但是prepare
方法中多了這一段,
if (mPrepareSync) {
mSignal.wait(mLock); // wait for prepare done
mPrepareSync = false;
}
在這裡呼叫了wait
方法進行了等待,所以使得java層達到同步呼叫的效果,然後在prepare完成之後會呼叫notify方法喚醒它,程式碼如下
void MediaPlayer::notify(int msg, int ext1, int ext2, const Parcel *obj)
{
...
case MEDIA_PREPARED:
ALOGV("prepared");
mCurrentState = MEDIA_PLAYER_PREPARED;
if (mPrepareSync) {
ALOGV("signal application thread");
mPrepareSync = false;
mPrepareStatus = NO_ERROR;
mSignal.signal();
}
break;
}
解決方法
通過看原始碼,果然可以確定是因為prepare方法會直接在當前執行緒去讀取資源,即使資原始檔是一個網路資源,當網路條件比較差即弱網情況下時,那麼發生ANR的機率就會十分高了,而且如果請求中斷或者檔案不完整,也會導致播放失敗,解決方法之一的話可以採用下面的方式去播放一個語音
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(url);
mediaPlayer.prepareAsync();
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mp.start();
}
});
但是為了使對網路資源下載的過程可控,還是推薦大家自己做判斷,使用自己的網路下載方式去下載資原始檔然後再將其的本地路徑交由MediaPlayer播放。
由於專案中的IM使用的是雲信的SDK,所以我們也不好改動他們的程式碼,就只好在呼叫sdk的方法前自己先做判斷,若是網路資源則先下載好才去呼叫sdk的方法,然後也向雲信反映了這個問題,他們也表示應該做容錯處理,應該會在後續版本改進吧。
作者:EoniJJ 連結:https://www.jianshu.com/p/8635e50858f4 來源:簡書 簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。