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

本文主要是排查Android一個(gè)播放語(yǔ)音問(wèn)題帶來(lái)的ANR異常以及有時(shí)播放失敗的Bug
閱讀本文大概需要花費(fèi)3分鐘。

引言

最近項(xiàng)目中的IM模塊收到反映,語(yǔ)音消息點(diǎn)了之后正在播放卻沒(méi)有聲音,有時(shí)甚至直接ANR異常,因項(xiàng)目中的IM采用的是網(wǎng)易的云信,所以第一時(shí)間請(qǐng)教了云信的技術(shù)人員,得到的回復(fù)是他們的SDK播放語(yǔ)音是直接封裝調(diào)用了系統(tǒng)的Api,沒(méi)有做任何處理。既然這樣,那就只好自己研究下問(wèn)題啦

問(wèn)題定位

首先從IM的SDK中的語(yǔ)音播放類(lèi)入手,發(fā)現(xiàn)確實(shí)是調(diào)用了Android的系統(tǒng)語(yǔ)音播放。

IM的SDK源碼

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

setDataSource

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


setDataSource的重載方法里對(duì)傳入的數(shù)據(jù)來(lái)源做了區(qū)分,最后調(diào)用了native的setDataSource方法。

然后我們看一下prepare方法

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


根據(jù)注釋可以看到,prepareAsync方法是異步的去準(zhǔn)備資源,基本驗(yàn)證了我們之前的猜想,因?yàn)樗麄冏罱K都是調(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方法,最終都是會(huì)調(diào)用prepareAsync_l(),但是prepare方法中多了這一段,

    if (mPrepareSync) {
        mSignal.wait(mLock);  // wait for prepare done
        mPrepareSync = false;
    }

在這里調(diào)用了wait方法進(jìn)行了等待,所以使得java層達(dá)到同步調(diào)用的效果,然后在prepare完成之后會(huì)調(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;
}

解決方法

通過(guò)看源碼,果然可以確定是因?yàn)閜repare方法會(huì)直接在當(dāng)前線程去讀取資源,即使資源文件是一個(gè)網(wǎng)絡(luò)資源,當(dāng)網(wǎng)絡(luò)條件比較差即弱網(wǎng)情況下時(shí),那么發(fā)生ANR的幾率就會(huì)十分高了,而且如果請(qǐng)求中斷或者文件不完整,也會(huì)導(dǎo)致播放失敗,解決方法之一的話可以采用下面的方式去播放一個(gè)語(yǔ)音

       MediaPlayer mediaPlayer = new MediaPlayer();
       mediaPlayer.setDataSource(url);
       mediaPlayer.prepareAsync();
       mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
           @Override
           public void onPrepared(MediaPlayer mp) {
               mp.start();
           }
       });

但是為了使對(duì)網(wǎng)絡(luò)資源下載的過(guò)程可控,還是推薦大家自己做判斷,使用自己的網(wǎng)絡(luò)下載方式去下載資源文件然后再將其的本地路徑交由MediaPlayer播放。

由于項(xiàng)目中的IM使用的是云信的SDK,所以我們也不好改動(dòng)他們的代碼,就只好在調(diào)用sdk的方法前自己先做判斷,若是網(wǎng)絡(luò)資源則先下載好才去調(diào)用sdk的方法,然后也向云信反映了這個(gè)問(wèn)題,他們也表示應(yīng)該做容錯(cuò)處理,應(yīng)該會(huì)在后續(xù)版本改進(jìn)吧。


如果覺(jué)得對(duì)你有所幫助,請(qǐng)點(diǎn)個(gè)贊,謝謝。你的鼓勵(lì)是我最大的動(dòng)力。
歡迎關(guān)注EoniJJ的簡(jiǎn)書(shū)

不定期與你分享關(guān)于Android開(kāi)發(fā)的點(diǎn)點(diǎn)滴滴。

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

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

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