Android语音消息播放(MediaPlayer) 踩坑
本文主要是排查Android一个播放语音问题带来的ANR异常以及有时播放失败的Bug
阅读本文大概需要花费3分钟。
引言
最近项目中的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的简书
不定期与你分享关于Android开发的点点滴滴。