Android Studio 4模拟器测试录音播放功能时App崩溃求助
问题:点击停止录音按钮时App崩溃重启(Pixel 3a API 30模拟器)
我在Pixel 3a API 30模拟器上测试一款录音App时,点击停止录音按钮App持续崩溃重启。以下是使用的Java代码(来自YouTube教程):
//申请运行时权限 if (!checkPermissionFromDevice()) requestPermission(); //初始化视图 pl_btn = (Button)findViewById(R.id.play_btn); rcrd_btn = (Button)findViewById(R.id.record_button); stp_rcrd_btn = (Button)findViewById(R.id.stop_record_btn); ps_btn = (Button)findViewById(R.id.pause_btn); //从Android M开始,需申请运行时权限 rcrd_btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (checkPermissionFromDevice()) { pathSave = Environment.getExternalStorageDirectory() .getAbsolutePath() + "/" + UUID.randomUUID().toString() + "audio_record.3gp"; setupMediaRecorder(); try { mediaRecorder.prepare(); mediaRecorder.start(); } catch (IOException e) { e.printStackTrace(); } pl_btn.setEnabled(false); ps_btn.setEnabled(false); rcrd_btn.setEnabled(false); stp_rcrd_btn.setEnabled(true); Toast.makeText(recording_and_play_test.this, "Recording...", Toast.LENGTH_SHORT).show(); } else { requestPermission(); } } }); stp_rcrd_btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mediaRecorder.stop(); stp_rcrd_btn.setEnabled(false); pl_btn.setEnabled(true); rcrd_btn.setEnabled(true); ps_btn.setEnabled(false); } }); pl_btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ps_btn.setEnabled(true); stp_rcrd_btn.setEnabled(false); rcrd_btn.setEnabled(false); mediaPlayer = new MediaPlayer(); try { mediaPlayer.setDataSource(pathSave); mediaPlayer.prepare(); }catch (IOException e){ e.printStackTrace(); } mediaPlayer.start(); Toast.makeText(recording_and_play_test.this, "Playing...", Toast.LENGTH_SHORT).show(); } }); ps_btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { stp_rcrd_btn.setEnabled(false); rcrd_btn.setEnabled(true); pl_btn.setEnabled(true); ps_btn.setEnabled(false); if (mediaPlayer != null){ mediaPlayer.stop(); mediaPlayer.release(); setupMediaRecorder(); } } }); } private void setupMediaRecorder() { mediaRecorder = new MediaRecorder(); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); mediaRecorder.setAudioEncoder(MediaRecorder.OutputFormat.AMR_NB); mediaRecorder.setOutputFile(pathSave); } private void requestPermission() { ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO },REQUEST_PERMISSION_CODE); }
崩溃日志如下:
E/MediaRecorder: stop called in an invalid state: 4 D/AndroidRuntime: Shutting down VM E/AndroidRuntime: FATAL EXCEPTION: main Process: com.example.adrsingingscope, PID: 7309 java.lang.IllegalStateException at android.media.MediaRecorder.stop(Native Method) at com.example.adrsingingscope.recording_and_play_test$2.onClick(recording_and_play_test.java:88) at android.view.View.performClick(View.java:7448) at android.view.View.performClickInternal(View.java:7425) at android.view.View.access$3600(View.java:810) at android.view.View$PerformClick.run(View.java:28305) at android.os.Handler.handleCallback(Handler.java:938) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:223) at android.app.ActivityThread.main(ActivityThread.java:7656) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
问题分析
日志里的E/MediaRecorder: stop called in an invalid state: 4是核心问题,状态4对应MediaRecorder.ERROR_INVALID_OPERATION,说明调用mediaRecorder.stop()时,Recorder处于不允许执行该操作的状态,主要原因有两个:
- 音频编码器配置错误:你的
setupMediaRecorder()里误用了MediaRecorder.OutputFormat.AMR_NB作为音频编码器,应该使用MediaRecorder.AudioEncoder下的常量,这会导致录制初始化失败,进而触发stop时的异常。 - 缺少状态检查:没有判断
mediaRecorder是否有效、是否处于录制状态就直接调用stop(),容易触发非法状态异常。
解决方案
1. 修复音频编码器配置错误
修改setupMediaRecorder()方法,替换正确的音频编码器常量:
private void setupMediaRecorder() { mediaRecorder = new MediaRecorder(); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); // 替换为AudioEncoder下的常量 mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); mediaRecorder.setOutputFile(pathSave); }
2. 安全调用stop(),添加状态检查与异常捕获
在停止录制的点击事件中,先验证mediaRecorder的有效性,同时捕获异常避免崩溃:
stp_rcrd_btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mediaRecorder != null) { try { mediaRecorder.stop(); // 停止后及时释放资源,避免重复操作出错 mediaRecorder.release(); mediaRecorder = null; } catch (IllegalStateException e) { e.printStackTrace(); Toast.makeText(recording_and_play_test.this, "录制未正常启动", Toast.LENGTH_SHORT).show(); } } // 更新按钮状态 stp_rcrd_btn.setEnabled(false); pl_btn.setEnabled(true); rcrd_btn.setEnabled(true); ps_btn.setEnabled(false); } });
3. 优化录制启动的异常处理
在录制启动时,捕获prepare()和start()的异常,及时反馈用户并重置按钮状态:
rcrd_btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (checkPermissionFromDevice()) { pathSave = Environment.getExternalStorageDirectory() .getAbsolutePath() + "/" + UUID.randomUUID().toString() + "audio_record.3gp"; setupMediaRecorder(); try { mediaRecorder.prepare(); mediaRecorder.start(); // 只有启动成功才更新按钮状态 pl_btn.setEnabled(false); ps_btn.setEnabled(false); rcrd_btn.setEnabled(false); stp_rcrd_btn.setEnabled(true); Toast.makeText(recording_and_play_test.this, "Recording...", Toast.LENGTH_SHORT).show(); } catch (IOException e) { e.printStackTrace(); Toast.makeText(recording_and_play_test.this, "录制启动失败:" + e.getMessage(), Toast.LENGTH_SHORT).show(); // 启动失败,重置按钮状态 rcrd_btn.setEnabled(true); stp_rcrd_btn.setEnabled(false); // 释放无效的recorder if (mediaRecorder != null) { mediaRecorder.release(); mediaRecorder = null; } } } else { requestPermission(); } } });
4. 补充权限申请回调
确保权限申请通过后再允许录制操作,添加onRequestPermissionsResult回调:
@Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_PERMISSION_CODE) { boolean allGranted = true; for (int result : grantResults) { if (result != PackageManager.PERMISSION_GRANTED) { allGranted = false; break; } } if (!allGranted) { Toast.makeText(this, "需要录音和存储权限才能录制", Toast.LENGTH_SHORT).show(); } } }
额外提示
- 在Android 10及以上版本,建议改用
getExternalFilesDir()存储录制文件,无需额外的WRITE_EXTERNAL_STORAGE权限:pathSave = getExternalFilesDir(Environment.DIRECTORY_MUSIC) .getAbsolutePath() + "/" + UUID.randomUUID().toString() + "audio_record.3gp"; - 在Activity的
onDestroy()方法中释放剩余的MediaRecorder和MediaPlayer资源,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Tim Huang




