本文主要是排查Android一個播放語音問題帶來的ANR異常以及有時播放失敗的Bug
閱讀本文大概需要花費3分鐘。
引言
最近項目中的IM模塊收到反映,語音消息點了之后正在播放卻沒有聲音,有時甚至直接ANR異常,因項目中的IM采用的是網(wǎng)易的云信,所以第一時間請教了云信的技術(shù)人員,得到的回復(fù)是他們的SDK播放語音是直接封裝調(diào)用了系統(tǒng)的Api,沒有做任何處理。既然這樣,那就只好自己研究下問題啦
問題定位
首先從IM的SDK中的語音播放類入手,發(fā)現(xiàn)確實是調(diào)用了Android的系統(tǒng)語音播放。

那么我們?nèi)タ匆幌翧ndroid的媒體播放類MediaPlayer的這幾個方法的源碼,分析一下問題,先看一下MediaPlayer的
setDataSource方法,

通過注釋可以看到,這個方法是支持傳遞本地文件路徑或者是一個網(wǎng)絡(luò)路徑的,猜測是否是因為在ui線程加載網(wǎng)絡(luò)資源,導(dǎo)致了anr,我們接著往下看

setDataSource的重載方法里對傳入的數(shù)據(jù)來源做了區(qū)分,最后調(diào)用了native的setDataSource方法。
然后我們看一下prepare方法

從注釋可以看到,prepare方法還有另外一個prepareAsync方法,

根據(jù)注釋可以看到,
prepareAsync方法是異步的去準備資源,基本驗證了我們之前的猜想,因為他們最終都是調(diào)用了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方法,最終都是會調(diào)用prepareAsync_l(),但是prepare方法中多了這一段,
if (mPrepareSync) {
mSignal.wait(mLock); // wait for prepare done
mPrepareSync = false;
}
在這里調(diào)用了wait方法進行了等待,所以使得java層達到同步調(diào)用的效果,然后在prepare完成之后會調(diào)用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方法會直接在當前線程去讀取資源,即使資源文件是一個網(wǎng)絡(luò)資源,當網(wǎng)絡(luò)條件比較差即弱網(wǎng)情況下時,那么發(fā)生ANR的幾率就會十分高了,而且如果請求中斷或者文件不完整,也會導(dǎo)致播放失敗,解決方法之一的話可以采用下面的方式去播放一個語音
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(url);
mediaPlayer.prepareAsync();
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mp.start();
}
});
但是為了使對網(wǎng)絡(luò)資源下載的過程可控,還是推薦大家自己做判斷,使用自己的網(wǎng)絡(luò)下載方式去下載資源文件然后再將其的本地路徑交由MediaPlayer播放。
由于項目中的IM使用的是云信的SDK,所以我們也不好改動他們的代碼,就只好在調(diào)用sdk的方法前自己先做判斷,若是網(wǎng)絡(luò)資源則先下載好才去調(diào)用sdk的方法,然后也向云信反映了這個問題,他們也表示應(yīng)該做容錯處理,應(yīng)該會在后續(xù)版本改進吧。
如果覺得對你有所幫助,請點個贊,謝謝。你的鼓勵是我最大的動力。
歡迎關(guān)注EoniJJ的簡書
不定期與你分享關(guān)于Android開發(fā)的點點滴滴。