Android屏幕旋转时MediaPlayer seekTo报错 无法保留播放进度
从堆栈信息可以直接定位到崩溃触发点:屏幕旋转触发Activity暂停流程时,在Ayuda.java:124行调用MediaPlayer.isPlaying()方法时抛出IllegalStateException。
这个异常来自Android MediaPlayer的状态机机制:MediaPlayer对不同状态下可调用的方法有严格限制,当实例处于Released(已释放)、Idle(刚重置未准备)、Error状态时,直接调用isPlaying()、getCurrentPosition()这类方法,就会直接触发崩溃。
结合代码逻辑,触发这个异常的具体场景是:
- 音频播放完成后,在
OnCompletionListener里调用了mediaPlayer.release(),此时MediaPlayer实例已经进入不可用的Released状态,但没有把这个实例的引用置空,也没有加状态判断 - 屏幕旋转时Activity走暂停生命周期流程,代码直接拿到已经被释放的MediaPlayer实例调用
isPlaying(),没有任何状态校验,直接触发崩溃 - 当前的旋转恢复播放逻辑本身存在设计漏洞:屏幕旋转会导致Activity被完全销毁重建,重建流程里会new一个全新的空MediaPlayer实例,就算存了之前的播放位置,这个新实例连音频数据源都还没从Firebase异步拉取完成,根本无法恢复播放进度。
1. 先解决无状态校验导致的崩溃
所有调用MediaPlayer方法的位置,必须增加前置状态判断,避免在不可用状态下调用方法。
- 增加两个标记位记录MediaPlayer的运行状态:是否被释放、是否完成准备,避免直接依赖实例判断状态
// 类成员变量 private boolean isPlayerReleased = false; private boolean isPlayerPrepared = false;
- 初始化MediaPlayer时重置状态标记,在准备完成回调里同步更新状态
mediaPlayer = new MediaPlayer(); isPlayerReleased = false; isPlayerPrepared = false; mediaPlayer.setOnPreparedListener(mp -> { isPlayerPrepared = true; // 准备完成,更新标记 mp.start(); }); mediaPlayer.setOnCompletionListener(mp -> { mp.release(); isPlayerReleased = true; // 标记实例已释放 isPlayerPrepared = false; mediaPlayer = null; // 置空引用,避免后续误调用 cargar.setEnabled(true); pausa.setEnabled(false); stop.setEnabled(false); });
- 封装安全的MediaPlayer调用方法,替代原来直接调用原生方法的逻辑
// 安全判断是否正在播放 private boolean isPlayerPlaying() { if (mediaPlayer == null || isPlayerReleased) return false; try { // 只有完成准备的状态才能调用isPlaying if (isPlayerPrepared) { return mediaPlayer.isPlaying(); } } catch (IllegalStateException e) { Log.e("MediaPlayer", "状态异常,无法获取播放状态"); } return false; } // 安全获取当前播放位置 private int getCurrentSafePosition() { if (mediaPlayer == null || isPlayerReleased) return 0; try { if (isPlayerPrepared) { return mediaPlayer.getCurrentPosition(); } } catch (IllegalStateException e) { Log.e("MediaPlayer", "状态异常,无法获取播放位置"); } return 0; }
- 把原来代码里所有直接调用
mediaPlayer.isPlaying()的位置(包括暂停按钮、停止按钮、onPause生命周期方法里的逻辑),全部替换为上面封装的安全方法。同时在加载音频的方法里,reset后同步重置准备标记:
public void cargarReproductor(View v) { mediaPlayer.reset(); isPlayerPrepared = false; // reset后回到空闲状态,标记未准备 try { mediaPlayer.setDataSource(ruta); } catch (IOException e) { e.printStackTrace(); } mediaPlayer.prepareAsync(); cargar.setEnabled(false); pausa.setEnabled(true); stop.setEnabled(true); }
2. 修复屏幕旋转时的播放恢复逻辑
当前实现根本无法实现旋转后续播,因为Activity重建时会生成全新的空MediaPlayer实例,完全丢失之前的播放状态。有两个成熟的修复方案可以选择:
方案一(最低改造成本):禁止屏幕旋转时重建Activity
在AndroidManifest.xml里给Ayuda这个Activity加上配置变更声明,让系统在发生屏幕旋转、屏幕尺寸变化、键盘弹出这类配置变更时,不销毁重建Activity,直接回调onConfigurationChanged方法处理配置变化。这样MediaPlayer实例会一直保留在内存里,播放进度、播放状态完全不受旋转影响,根本不需要手动存进度恢复。
配置示例:
<activity android:name=".views.Ayuda" android:configChanges="orientation|screenSize|keyboardHidden" />
方案二(更规范的架构实现):用ViewModel持有MediaPlayer实例
如果需要Activity响应配置变化的逻辑,可以把MediaPlayer的初始化、状态管理逻辑全部放到ViewModel里。ViewModel的生命周期比Activity长,Activity被销毁重建时ViewModel不会被销毁,会一直持有MediaPlayer实例,重建完成的Activity只需要从ViewModel里拿到MediaPlayer实例,就可以直接恢复之前的播放状态,不会生成空实例。
3. 补充状态存储的兜底逻辑
如果选择走方案一之外的路径,要在onSaveInstanceState里同步存上音频的远程路径,不要只存播放位置:
@Override protected void onSaveInstanceState(Bundle estadoGuardado) { super.onSaveInstanceState(estadoGuardado); estadoGuardado.putInt("posicion", getCurrentSafePosition()); estadoGuardado.putString("audio_ruta", ruta); // 把音频路径也存下来,避免Firebase异步拉取的延迟导致重建时拿不到路径 }
这样就算MediaPlayer实例需要重新初始化,也能快速拿到之前存的路径和进度,完成播放恢复。
内容的提问来源于stack exchange,提问作者Javi Ruiz

