Android語音消息播放(MediaPlayer) 踩坑

本文主要是排查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)語音播放。

IM的SDK源碼

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

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ā)的點點滴滴。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容